koishi-plugin-group-analysis-mx 1.2.4 → 1.2.6
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 +3 -3
- package/lib/service/analysis.js +101 -27
- package/package.json +1 -1
package/lib/config.js
CHANGED
|
@@ -301,7 +301,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
301
301
|
|
|
302
302
|
对于每个金句,请提供:
|
|
303
303
|
1. 原文内容(完整保留发言细节)
|
|
304
|
-
2.
|
|
304
|
+
2. 发送者标识(群聊记录中对应的 [user_id],原样复述)
|
|
305
305
|
3. 选择理由(具体说明其"逆天"之处,如逻辑颠覆点/脑洞角度/反差感/争议话题元素)
|
|
306
306
|
|
|
307
307
|
此外,我将对你进行严格约束:
|
|
@@ -316,14 +316,14 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
316
316
|
|
|
317
317
|
要求:生成内容时需优先围绕上述关键词、话题、昵称与时间范围;不相关的金句请过滤。如果为空的,请忽略。
|
|
318
318
|
|
|
319
|
-
|
|
319
|
+
群聊记录(每条消息的发送者用 [user_id] 标识,不含昵称):
|
|
320
320
|
{messages}
|
|
321
321
|
|
|
322
322
|
请严格按照以下 YAML 格式返回,放在 markdown 代码块中:
|
|
323
323
|
\`\`\`yaml
|
|
324
324
|
- content: |-
|
|
325
325
|
金句原文
|
|
326
|
-
sender: "
|
|
326
|
+
sender: "发送者的 [user_id](从群聊记录中对应的 [user_id] 原样选取)"
|
|
327
327
|
reason: |-
|
|
328
328
|
选择这句话的理由(需明确说明逆天特质,不要在里面添加任何markdown语法,请使用纯文本)
|
|
329
329
|
\`\`\``),
|
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
|
|
@@ -787,6 +814,18 @@ class AnalysisService extends koishi_1.Service {
|
|
|
787
814
|
this.ctx.logger.info(`开始分析 ${messages.length} 条消息...`);
|
|
788
815
|
const { userStats, totalChars, totalEmojiCount, allMessagesText } = (0, utils_1.calculateBasicStats)(messages);
|
|
789
816
|
const messagesText = allMessagesText.join('\n');
|
|
817
|
+
// ID 版消息文本:用 [user_id] 代替昵称,避免 emoji 昵称干扰 LLM;分析后按 ID 回填完整昵称。
|
|
818
|
+
const idBasedMessagesText = messages
|
|
819
|
+
.map((m) => {
|
|
820
|
+
const text = koishi_1.h
|
|
821
|
+
.select(m.elements || [], 'text')
|
|
822
|
+
.map((e) => e.attrs?.content || '')
|
|
823
|
+
.join('')
|
|
824
|
+
.trim();
|
|
825
|
+
return text ? `[${m.userId ?? 'unknown'}]: ${text}` : null;
|
|
826
|
+
})
|
|
827
|
+
.filter(Boolean)
|
|
828
|
+
.join('\n');
|
|
790
829
|
// LLM analyses in parallel
|
|
791
830
|
const users = Object.values(userStats);
|
|
792
831
|
const [topics, userTitles, goldenQuotes] = await Promise.all([
|
|
@@ -794,7 +833,7 @@ class AnalysisService extends koishi_1.Service {
|
|
|
794
833
|
this.config.userTitleAnalysis
|
|
795
834
|
? this.ctx.group_analysis_llm.analyzeUserTitles(users, context)
|
|
796
835
|
: Promise.resolve([]),
|
|
797
|
-
this.ctx.group_analysis_llm.analyzeGoldenQuotes(messagesText, this.config.maxGoldenQuotes, context)
|
|
836
|
+
this.ctx.group_analysis_llm.analyzeGoldenQuotes(idBasedMessagesText || messagesText, this.config.maxGoldenQuotes, context)
|
|
798
837
|
]).catch((error) => {
|
|
799
838
|
this.ctx.logger.error('LLM analysis failed:', error);
|
|
800
839
|
// On LLM failure, return empty results to avoid crashing the entire analysis.
|
|
@@ -806,9 +845,23 @@ class AnalysisService extends koishi_1.Service {
|
|
|
806
845
|
});
|
|
807
846
|
// Final statistics
|
|
808
847
|
const sortedUsers = users.sort((a, b) => b.messageCount - a.messageCount);
|
|
848
|
+
// 群名片解析:官方 QQ bot 消息作者名是"个人昵称",群名片需通过 getGuildMember 获取。
|
|
849
|
+
// 只对活跃的 top 用户调用,且带缓存避免重复请求。
|
|
850
|
+
const botForAvatar = this._getBot(selfId);
|
|
851
|
+
const guildIdForNick = target?.guildId;
|
|
852
|
+
const groupNicknameMap = new Map();
|
|
853
|
+
if (botForAvatar?.getGuildMember && guildIdForNick) {
|
|
854
|
+
const topUsers = sortedUsers.slice(0, Math.max(this.config.maxUsersInReport * 2, 20));
|
|
855
|
+
await Promise.all(topUsers.map(async (u) => {
|
|
856
|
+
const gname = await this.resolveGroupNickname(botForAvatar, guildIdForNick, String(u.userId));
|
|
857
|
+
if (gname)
|
|
858
|
+
groupNicknameMap.set(String(u.userId), gname);
|
|
859
|
+
}));
|
|
860
|
+
if (groupNicknameMap.size)
|
|
861
|
+
this.ctx.logger.info(`已获取 ${groupNicknameMap.size} 个群成员的群名片。`);
|
|
862
|
+
}
|
|
809
863
|
// 为用户补全真实头像:优先消息记录中的头像,其次按平台生成头像 URL。
|
|
810
864
|
// 官方 QQ 机器人等平台下 userId 为 openid,不能直接使用 QQ 号头像接口。
|
|
811
|
-
const botForAvatar = this._getBot(selfId);
|
|
812
865
|
const avatarMap = new Map();
|
|
813
866
|
for (const user of sortedUsers) {
|
|
814
867
|
if (!user.avatar) {
|
|
@@ -816,23 +869,26 @@ class AnalysisService extends koishi_1.Service {
|
|
|
816
869
|
}
|
|
817
870
|
avatarMap.set(String(user.userId), user.avatar || '');
|
|
818
871
|
}
|
|
819
|
-
//
|
|
820
|
-
//
|
|
872
|
+
// 昵称映射:统一用群名片作为显示名。LLM 生成的 sender/name 很可能是"个人昵称",
|
|
873
|
+
// 这里把个人昵称(含/不含 emoji)归一化后映射到群名片。
|
|
821
874
|
const nicknameAvatarMap = new Map();
|
|
822
875
|
const nicknameMap = new Map();
|
|
823
876
|
for (const user of sortedUsers) {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
877
|
+
const rawName = user.nickname;
|
|
878
|
+
const displayName = groupNicknameMap.get(String(user.userId)) || rawName;
|
|
879
|
+
if (rawName) {
|
|
880
|
+
const normRaw = (0, utils_1.normalizeNickname)(rawName);
|
|
881
|
+
nicknameMap.set(normRaw || rawName, displayName);
|
|
882
|
+
nicknameMap.set((0, utils_1.normalizeNickname)(displayName) || displayName, displayName);
|
|
883
|
+
}
|
|
884
|
+
if (user.avatar) {
|
|
885
|
+
nicknameAvatarMap.set(rawName, user.avatar);
|
|
886
|
+
const normRaw = (0, utils_1.normalizeNickname)(rawName);
|
|
887
|
+
if (normRaw)
|
|
888
|
+
nicknameAvatarMap.set(normRaw, user.avatar);
|
|
889
|
+
}
|
|
890
|
+
// 排行榜显示名使用群名片
|
|
891
|
+
user.nickname = displayName;
|
|
836
892
|
}
|
|
837
893
|
// 用户称号:用真实昵称替换 LLM 生成的 name(保留 emoji),并补全头像
|
|
838
894
|
if (Array.isArray(userTitles)) {
|
|
@@ -850,21 +906,39 @@ class AnalysisService extends koishi_1.Service {
|
|
|
850
906
|
}
|
|
851
907
|
});
|
|
852
908
|
}
|
|
853
|
-
//
|
|
909
|
+
// 金句:LLM 输入用 [user_id],这里按 ID 精确回填完整昵称(含 emoji),并补全头像。
|
|
854
910
|
if (Array.isArray(goldenQuotes)) {
|
|
911
|
+
// id → 完整昵称(群名片优先,回退消息昵称)
|
|
912
|
+
const idToNickname = new Map();
|
|
913
|
+
for (const msg of messages) {
|
|
914
|
+
if (msg.userId && msg.username)
|
|
915
|
+
idToNickname.set(String(msg.userId), msg.username);
|
|
916
|
+
}
|
|
917
|
+
for (const [uid, gname] of groupNicknameMap)
|
|
918
|
+
idToNickname.set(uid, gname);
|
|
855
919
|
goldenQuotes.forEach((quote) => {
|
|
856
920
|
if (quote && quote.sender) {
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
921
|
+
const uid = String(quote.sender).replace(/[\[\]]/g, '').trim();
|
|
922
|
+
if (idToNickname.has(uid)) {
|
|
923
|
+
// LLM 返回的是 [user_id],精确回填完整昵称(含 emoji)
|
|
924
|
+
quote.sender = idToNickname.get(uid);
|
|
925
|
+
if (!quote.avatar && avatarMap.has(uid))
|
|
926
|
+
quote.avatar = avatarMap.get(uid);
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
// 兜底:LLM 可能返回了昵称,按归一化匹配
|
|
930
|
+
const normalized = (0, utils_1.normalizeNickname)(quote.sender);
|
|
931
|
+
const realName = nicknameMap.get(quote.sender) ||
|
|
932
|
+
(normalized ? nicknameMap.get(normalized) : '') ||
|
|
867
933
|
'';
|
|
934
|
+
if (realName)
|
|
935
|
+
quote.sender = realName;
|
|
936
|
+
if (!quote.avatar) {
|
|
937
|
+
quote.avatar =
|
|
938
|
+
nicknameAvatarMap.get(quote.sender) ||
|
|
939
|
+
nicknameAvatarMap.get(normalized) ||
|
|
940
|
+
'';
|
|
941
|
+
}
|
|
868
942
|
}
|
|
869
943
|
}
|
|
870
944
|
});
|
package/package.json
CHANGED