koishi-plugin-group-analysis-mx 1.0.1
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.d.ts +53 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +376 -0
- package/lib/config.js.map +1 -0
- package/lib/cron.d.ts +8 -0
- package/lib/cron.d.ts.map +1 -0
- package/lib/cron.js +62 -0
- package/lib/cron.js.map +1 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +51 -0
- package/lib/index.js.map +1 -0
- package/lib/onebot/cqcode.d.ts +14 -0
- package/lib/onebot/cqcode.d.ts.map +1 -0
- package/lib/onebot/cqcode.js +92 -0
- package/lib/onebot/cqcode.js.map +1 -0
- package/lib/plugin.d.ts +4 -0
- package/lib/plugin.d.ts.map +1 -0
- package/lib/plugin.js +41 -0
- package/lib/plugin.js.map +1 -0
- package/lib/plugins/command.d.ts +9 -0
- package/lib/plugins/command.d.ts.map +1 -0
- package/lib/plugins/command.js +196 -0
- package/lib/plugins/command.js.map +1 -0
- package/lib/service/analysis.d.ts +48 -0
- package/lib/service/analysis.d.ts.map +1 -0
- package/lib/service/analysis.js +714 -0
- package/lib/service/analysis.js.map +1 -0
- package/lib/service/llm.d.ts +43 -0
- package/lib/service/llm.d.ts.map +1 -0
- package/lib/service/llm.js +160 -0
- package/lib/service/llm.js.map +1 -0
- package/lib/service/message.d.ts +55 -0
- package/lib/service/message.d.ts.map +1 -0
- package/lib/service/message.js +527 -0
- package/lib/service/message.js.map +1 -0
- package/lib/service/renderer.d.ts +23 -0
- package/lib/service/renderer.d.ts.map +1 -0
- package/lib/service/renderer.js +298 -0
- package/lib/service/renderer.js.map +1 -0
- package/lib/skins/anime.d.ts +19 -0
- package/lib/skins/anime.d.ts.map +1 -0
- package/lib/skins/anime.js +138 -0
- package/lib/skins/anime.js.map +1 -0
- package/lib/skins/art.d.ts +19 -0
- package/lib/skins/art.d.ts.map +1 -0
- package/lib/skins/art.js +146 -0
- package/lib/skins/art.js.map +1 -0
- package/lib/skins/index.d.ts +50 -0
- package/lib/skins/index.d.ts.map +1 -0
- package/lib/skins/index.js +84 -0
- package/lib/skins/index.js.map +1 -0
- package/lib/skins/md3.d.ts +19 -0
- package/lib/skins/md3.d.ts.map +1 -0
- package/lib/skins/md3.js +147 -0
- package/lib/skins/md3.js.map +1 -0
- package/lib/skins/newspaper.d.ts +19 -0
- package/lib/skins/newspaper.d.ts.map +1 -0
- package/lib/skins/newspaper.js +141 -0
- package/lib/skins/newspaper.js.map +1 -0
- package/lib/skins/scrapbook.d.ts +20 -0
- package/lib/skins/scrapbook.d.ts.map +1 -0
- package/lib/skins/scrapbook.js +207 -0
- package/lib/skins/scrapbook.js.map +1 -0
- package/lib/skins/types.d.ts +69 -0
- package/lib/skins/types.d.ts.map +1 -0
- package/lib/skins/types.js +12 -0
- package/lib/skins/types.js.map +1 -0
- package/lib/types.d.ts +170 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +3 -0
- package/lib/types.js.map +1 -0
- package/lib/utils.d.ts +55 -0
- package/lib/utils.d.ts.map +1 -0
- package/lib/utils.js +334 -0
- package/lib/utils.js.map +1 -0
- package/package.json +48 -0
- package/resources/anime/css/template_group.css +470 -0
- package/resources/anime/images/decoration_br.svg +19 -0
- package/resources/anime/images/decoration_tl.svg +24 -0
- package/resources/anime/template_group.html +108 -0
- package/resources/anime/template_user.html +99 -0
- package/resources/art/css/template_group.css +484 -0
- package/resources/art/template_group.html +77 -0
- package/resources/art/template_user.html +63 -0
- package/resources/md3/css/template_group.css +880 -0
- package/resources/md3/template_group.html +189 -0
- package/resources/md3/template_user.html +175 -0
- package/resources/newspaper/css/template_group.css +568 -0
- package/resources/newspaper/template_group.html +133 -0
- package/resources/newspaper/template_user.html +119 -0
- package/resources/scrapbook/css/style.css +1094 -0
- package/resources/scrapbook/template_group.html +115 -0
- package/resources/scrapbook/template_user.html +119 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AnalysisService = void 0;
|
|
4
|
+
const koishi_1 = require("koishi");
|
|
5
|
+
const js_yaml_1 = require("js-yaml");
|
|
6
|
+
const utils_1 = require("../utils");
|
|
7
|
+
class AnalysisService extends koishi_1.Service {
|
|
8
|
+
config;
|
|
9
|
+
static inject = [
|
|
10
|
+
'group_analysis_llm',
|
|
11
|
+
'group_analysis_message',
|
|
12
|
+
'group_analysis_renderer'
|
|
13
|
+
];
|
|
14
|
+
personaCache = new Map();
|
|
15
|
+
personaProcessing = new Set();
|
|
16
|
+
groupAnalysisCache = new Map();
|
|
17
|
+
constructor(ctx, config) {
|
|
18
|
+
super(ctx, 'group_analysis', true);
|
|
19
|
+
this.config = config;
|
|
20
|
+
this.setupPersonaDatabase();
|
|
21
|
+
this.setupPersonaMessageListener();
|
|
22
|
+
}
|
|
23
|
+
setupPersonaDatabase() {
|
|
24
|
+
this.ctx.database.extend('group_analysis_user_personas', {
|
|
25
|
+
id: {
|
|
26
|
+
type: 'char',
|
|
27
|
+
length: 100
|
|
28
|
+
},
|
|
29
|
+
platform: {
|
|
30
|
+
type: 'char',
|
|
31
|
+
length: 30
|
|
32
|
+
},
|
|
33
|
+
selfId: { type: 'char', length: 100 },
|
|
34
|
+
userId: { type: 'char', length: 100 },
|
|
35
|
+
username: { type: 'char', length: 254 },
|
|
36
|
+
persona: { type: 'text', nullable: true },
|
|
37
|
+
lastAnalysisAt: { type: 'timestamp', nullable: true },
|
|
38
|
+
updatedAt: { type: 'timestamp', nullable: true }
|
|
39
|
+
}, {
|
|
40
|
+
primary: 'id'
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
setupPersonaMessageListener() {
|
|
44
|
+
if (this.config.personaAnalysisMessageInterval === 0) {
|
|
45
|
+
this.ctx.logger.info('已关闭自动用户画像分析。');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.ctx.group_analysis_message.onUserMessage(async (session) => {
|
|
49
|
+
if (!(0, utils_1.shouldListenToMessage)(session, this.config.listenerGroups, this.config.enableAllGroupsByDefault))
|
|
50
|
+
return;
|
|
51
|
+
await this.handleIncomingMessageForPersona(session);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async handleIncomingMessageForPersona(session) {
|
|
55
|
+
if (this.config.personaAnalysisMessageInterval === 0)
|
|
56
|
+
return;
|
|
57
|
+
if (!session.userId)
|
|
58
|
+
return;
|
|
59
|
+
// Skip users in personaUserFilter
|
|
60
|
+
if (this.config.personaUserFilter.includes(session.userId)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const recordId = (0, utils_1.buildPersonaRecordId)(session.platform, session.selfId, session.userId);
|
|
64
|
+
const cache = await this.ensurePersonaCache(recordId, {
|
|
65
|
+
platform: session.platform,
|
|
66
|
+
selfId: session.selfId,
|
|
67
|
+
userId: session.userId,
|
|
68
|
+
username: session.username || session.userId
|
|
69
|
+
});
|
|
70
|
+
cache.pendingMessages += 1;
|
|
71
|
+
cache.record.username = session.username || cache.record.username;
|
|
72
|
+
if (cache.pendingMessages >=
|
|
73
|
+
this.config.personaAnalysisMessageInterval &&
|
|
74
|
+
!this.personaProcessing.has(recordId)) {
|
|
75
|
+
this.personaProcessing.add(recordId);
|
|
76
|
+
const sourceGroup = {
|
|
77
|
+
guildId: session.guildId || undefined,
|
|
78
|
+
channelId: session.channelId || undefined,
|
|
79
|
+
platform: session.platform,
|
|
80
|
+
selfId: session.selfId
|
|
81
|
+
};
|
|
82
|
+
// eslint-disable-next-line no-void
|
|
83
|
+
void this.runPersonaAnalysis(cache, sourceGroup)
|
|
84
|
+
.catch((error) => this.ctx.logger.error(`执行用户画像分析失败 (${recordId}):`, error))
|
|
85
|
+
.finally(() => {
|
|
86
|
+
this.personaProcessing.delete(recordId);
|
|
87
|
+
cache.pendingMessages = 0;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async ensurePersonaCache(id, defaults) {
|
|
92
|
+
const cached = this.personaCache.get(id);
|
|
93
|
+
if (cached)
|
|
94
|
+
return cached;
|
|
95
|
+
const existing = await this.ctx.database
|
|
96
|
+
.select('group_analysis_user_personas')
|
|
97
|
+
.where({ id })
|
|
98
|
+
.execute()
|
|
99
|
+
.then((records) => records[0]);
|
|
100
|
+
if (existing) {
|
|
101
|
+
if (existing.lastAnalysisAt &&
|
|
102
|
+
!(existing.lastAnalysisAt instanceof Date)) {
|
|
103
|
+
existing.lastAnalysisAt = new Date(existing.lastAnalysisAt);
|
|
104
|
+
}
|
|
105
|
+
if (existing.updatedAt && !(existing.updatedAt instanceof Date)) {
|
|
106
|
+
existing.updatedAt = new Date(existing.updatedAt);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
let parsedPersona;
|
|
110
|
+
if (existing?.persona) {
|
|
111
|
+
try {
|
|
112
|
+
parsedPersona = (0, js_yaml_1.load)(existing.persona);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
this.ctx.logger.warn(`解析用户画像 YAML 失败 (${id}),将忽略历史画像。`, error);
|
|
116
|
+
parsedPersona = null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const cache = {
|
|
120
|
+
record: existing ||
|
|
121
|
+
{
|
|
122
|
+
id,
|
|
123
|
+
...defaults
|
|
124
|
+
},
|
|
125
|
+
pendingMessages: 0,
|
|
126
|
+
parsedPersona
|
|
127
|
+
};
|
|
128
|
+
/* if (!existing) {
|
|
129
|
+
await this.ctx.database.create(
|
|
130
|
+
'group_analysis_user_personas',
|
|
131
|
+
cache.record
|
|
132
|
+
)
|
|
133
|
+
} */
|
|
134
|
+
this.personaCache.set(id, cache);
|
|
135
|
+
return cache;
|
|
136
|
+
}
|
|
137
|
+
async runPersonaAnalysis(cache, sourceGroup) {
|
|
138
|
+
const { record } = cache;
|
|
139
|
+
const lookbackStart = (0, utils_1.getStartTimeByDays)(this.config.personaLookbackDays);
|
|
140
|
+
const historyMessages = await this.collectUserMessagesForPersona(record, lookbackStart, sourceGroup);
|
|
141
|
+
if (historyMessages.length < this.config.personaMinMessages) {
|
|
142
|
+
this.ctx.logger.info(`用户 ${record.userId} 在设定时间窗内仅收集到 ${historyMessages.length} 条消息,低于触发阈值 ${this.config.personaMinMessages},跳过画像分析。`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const promptMessages = (0, utils_1.formatMessagesForPersona)(historyMessages);
|
|
146
|
+
this.ctx.logger.info(`开始分析用户 ${record.userId} 的画像 (${record.username}),收集到 ${historyMessages.length} 条消息。`);
|
|
147
|
+
const previousText = (0, utils_1.formatPersonaForPrompt)(cache.parsedPersona);
|
|
148
|
+
const persona = await this.ctx.group_analysis_llm.analyzeUserPersona(record.userId, record.username, record.roles, promptMessages, previousText);
|
|
149
|
+
if (!persona) {
|
|
150
|
+
this.ctx.logger.warn(`LLM 未返回用户画像结果 (${record.userId})。`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const personaWithEvidence = this.attachEvidenceMessageIds(persona, historyMessages);
|
|
154
|
+
const merged = (0, utils_1.mergePersona)(cache.parsedPersona, personaWithEvidence);
|
|
155
|
+
cache.parsedPersona = merged;
|
|
156
|
+
await this.persistPersona(record, merged);
|
|
157
|
+
}
|
|
158
|
+
async collectUserMessagesForPersona(record, startTime, sourceGroup) {
|
|
159
|
+
const results = [];
|
|
160
|
+
const relevantGroups = this.config.enableAllGroupsByDefault
|
|
161
|
+
? [
|
|
162
|
+
{
|
|
163
|
+
enabled: true,
|
|
164
|
+
platform: sourceGroup?.platform,
|
|
165
|
+
selfId: sourceGroup?.selfId,
|
|
166
|
+
channelId: sourceGroup?.channelId,
|
|
167
|
+
guildId: sourceGroup?.guildId
|
|
168
|
+
}
|
|
169
|
+
].filter((group) => !!group.platform &&
|
|
170
|
+
!!group.selfId &&
|
|
171
|
+
(!!group.channelId || !!group.guildId))
|
|
172
|
+
: this.config.listenerGroups.filter((group) => group.enabled &&
|
|
173
|
+
group.platform === record.platform &&
|
|
174
|
+
group.selfId === record.selfId);
|
|
175
|
+
if (!relevantGroups.length) {
|
|
176
|
+
const reason = this.config.enableAllGroupsByDefault
|
|
177
|
+
? '未提供可用的当前群组信息'
|
|
178
|
+
: '未在配置中找到监听群组';
|
|
179
|
+
this.ctx.logger.warn(`用户 ${record.userId} 画像分析缺少群组上下文(${reason}),跳过画像分析。`);
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
const totalLimit = this.config.personaMaxMessages;
|
|
183
|
+
for (const group of relevantGroups) {
|
|
184
|
+
const bot = this._getBot(group.selfId);
|
|
185
|
+
let userGroupInfo = null;
|
|
186
|
+
try {
|
|
187
|
+
if (bot.platform === 'onebot') {
|
|
188
|
+
userGroupInfo = await bot.internal.getGroupMemberInfo(group.guildId, record.userId, true);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
userGroupInfo = await bot.getGuildMember(group.channelId || group.guildId, record.userId);
|
|
192
|
+
}
|
|
193
|
+
if (userGroupInfo == null) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
this.ctx.logger.warn(`获取用户 ${record.userId} 的群组信息失败 (${group.channelId || group.guildId}),可能是未加入该群聊。将跳过此群组的信息获取。`, error);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
record.roles = (userGroupInfo.roles || []).map((r) => (typeof r === 'string' ? r : r.name || r.id || ''));
|
|
202
|
+
const history = await this.ctx.group_analysis_message.getHistoricalMessages({
|
|
203
|
+
guildId: group.guildId,
|
|
204
|
+
channelId: group.channelId,
|
|
205
|
+
userId: [record.userId],
|
|
206
|
+
selfId: group.selfId,
|
|
207
|
+
startTime,
|
|
208
|
+
endTime: new Date(),
|
|
209
|
+
limit: totalLimit,
|
|
210
|
+
purpose: 'user-persona'
|
|
211
|
+
});
|
|
212
|
+
results.push(...history.map((message) => ({
|
|
213
|
+
...message,
|
|
214
|
+
guildId: message.guildId ?? group.guildId,
|
|
215
|
+
channelId: message.channelId ?? group.channelId
|
|
216
|
+
})));
|
|
217
|
+
if (results.length >= totalLimit) {
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
results.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
222
|
+
return results;
|
|
223
|
+
}
|
|
224
|
+
attachEvidenceMessageIds(persona, messages) {
|
|
225
|
+
if (!persona.evidence) {
|
|
226
|
+
return persona;
|
|
227
|
+
}
|
|
228
|
+
const evidenceEntries = persona.evidence;
|
|
229
|
+
const messageIndex = {};
|
|
230
|
+
for (const message of messages) {
|
|
231
|
+
const key = message.messageId || message.id;
|
|
232
|
+
if (key && !messageIndex[key]) {
|
|
233
|
+
messageIndex[message.messageId] = message;
|
|
234
|
+
messageIndex[message.id] = message;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const updated = [];
|
|
238
|
+
for (const entry of evidenceEntries) {
|
|
239
|
+
const resolvedMessage = messageIndex[entry];
|
|
240
|
+
if (resolvedMessage) {
|
|
241
|
+
updated.push(koishi_1.h
|
|
242
|
+
.select(resolvedMessage.elements || [
|
|
243
|
+
koishi_1.h.text(resolvedMessage.content)
|
|
244
|
+
], 'text')
|
|
245
|
+
.toString());
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
this.ctx.logger.warn(`无法找到画像证据 ${entry} 对应的消息,请检查消息服务是否正常。`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
this.ctx.logger.info(`已解析 ${updated.length} 条画像证据。`);
|
|
252
|
+
return {
|
|
253
|
+
...persona,
|
|
254
|
+
evidence: updated.length ? updated : []
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async persistPersona(record, persona) {
|
|
258
|
+
const now = new Date();
|
|
259
|
+
record.persona = (0, js_yaml_1.dump)(persona, {
|
|
260
|
+
indent: 2,
|
|
261
|
+
lineWidth: -1,
|
|
262
|
+
noRefs: true
|
|
263
|
+
});
|
|
264
|
+
record.lastAnalysisAt = now;
|
|
265
|
+
record.updatedAt = now;
|
|
266
|
+
await this.ctx.database.upsert('group_analysis_user_personas', [record]);
|
|
267
|
+
}
|
|
268
|
+
async _getGroupHistoryFromMessageService(selfId, target, days) {
|
|
269
|
+
const targetId = target.guildId || target.channelId;
|
|
270
|
+
if (!targetId) {
|
|
271
|
+
this.ctx.logger.warn('执行群分析时缺少有效的群组或频道标识。');
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
this.ctx.logger.info(`开始从消息服务获取群组 ${targetId} 近 ${days} 天的消息记录...`);
|
|
275
|
+
const startTime = (0, utils_1.getStartTimeByDays)(days);
|
|
276
|
+
const endTime = new Date();
|
|
277
|
+
const messages = await this.ctx.group_analysis_message.getHistoricalMessages({
|
|
278
|
+
guildId: target.guildId,
|
|
279
|
+
channelId: target.channelId,
|
|
280
|
+
startTime,
|
|
281
|
+
selfId,
|
|
282
|
+
endTime,
|
|
283
|
+
limit: this.config.maxMessages,
|
|
284
|
+
purpose: 'group-analysis'
|
|
285
|
+
});
|
|
286
|
+
this.ctx.logger.info(`从消息服务获取到 ${messages.length} 条消息。`);
|
|
287
|
+
return messages;
|
|
288
|
+
}
|
|
289
|
+
async _getGroupHistoryFromMessageServiceByTimeRange(selfId, target, startTime, endTime) {
|
|
290
|
+
const targetId = target.guildId || target.channelId;
|
|
291
|
+
if (!targetId) {
|
|
292
|
+
this.ctx.logger.warn('执行群分析时缺少有效的群组或频道标识。');
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
this.ctx.logger.info(`开始从消息服务获取群组 ${targetId} ${startTime.toLocaleString()} - ${endTime.toLocaleString()} 的消息记录...`);
|
|
296
|
+
const messages = await this.ctx.group_analysis_message.getHistoricalMessages({
|
|
297
|
+
guildId: target.guildId,
|
|
298
|
+
channelId: target.channelId,
|
|
299
|
+
startTime,
|
|
300
|
+
selfId,
|
|
301
|
+
endTime,
|
|
302
|
+
limit: this.config.maxMessages,
|
|
303
|
+
purpose: 'group-analysis'
|
|
304
|
+
});
|
|
305
|
+
this.ctx.logger.info(`从消息服务获取到 ${messages.length} 条消息。`);
|
|
306
|
+
return messages;
|
|
307
|
+
}
|
|
308
|
+
async resolveGroupName(selfId, target) {
|
|
309
|
+
const bot = this._getBot(selfId);
|
|
310
|
+
const fallbackName = target.guildId || target.channelId || 'unknown';
|
|
311
|
+
let groupName = fallbackName;
|
|
312
|
+
if (bot) {
|
|
313
|
+
try {
|
|
314
|
+
if (target.guildId) {
|
|
315
|
+
groupName =
|
|
316
|
+
(await bot.getGuild(target.guildId)).name || groupName;
|
|
317
|
+
}
|
|
318
|
+
else if (target.channelId && bot.getChannel) {
|
|
319
|
+
const channel = await bot.getChannel(target.channelId, target.guildId);
|
|
320
|
+
groupName = channel?.name || groupName;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
this.ctx.logger.warn(`获取群组 ${fallbackName} 名称失败: ${err}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return groupName;
|
|
328
|
+
}
|
|
329
|
+
normalizeQueryAction(action) {
|
|
330
|
+
const normalized = action?.trim();
|
|
331
|
+
if (normalized === '分析加对话' || normalized === '只对话') {
|
|
332
|
+
return normalized;
|
|
333
|
+
}
|
|
334
|
+
return '只分析';
|
|
335
|
+
}
|
|
336
|
+
resolveQueryTimeRange(intent, fallbackDays) {
|
|
337
|
+
const now = new Date();
|
|
338
|
+
let start;
|
|
339
|
+
let end;
|
|
340
|
+
if (intent?.targetTime?.startTime) {
|
|
341
|
+
const parsed = new Date(intent.targetTime.startTime);
|
|
342
|
+
if (!isNaN(parsed.getTime()))
|
|
343
|
+
start = parsed;
|
|
344
|
+
}
|
|
345
|
+
if (intent?.targetTime?.endTime) {
|
|
346
|
+
const parsed = new Date(intent.targetTime.endTime);
|
|
347
|
+
if (!isNaN(parsed.getTime()))
|
|
348
|
+
end = parsed;
|
|
349
|
+
}
|
|
350
|
+
if (!end)
|
|
351
|
+
end = now;
|
|
352
|
+
if (!start)
|
|
353
|
+
start = (0, utils_1.getStartTimeByDays)(fallbackDays);
|
|
354
|
+
if (start > end) {
|
|
355
|
+
const temp = start;
|
|
356
|
+
start = end;
|
|
357
|
+
end = temp;
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
start,
|
|
361
|
+
end,
|
|
362
|
+
description: intent?.targetTime?.description
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
async executeGroupAnalysis(selfId, target, days, outputFormat, force) {
|
|
366
|
+
const bot = this._getBot(selfId);
|
|
367
|
+
const targetChannel = target.channelId ?? target.guildId;
|
|
368
|
+
const targetGuildContext = target.channelId && target.guildId ? target.guildId : undefined;
|
|
369
|
+
if (!targetChannel) {
|
|
370
|
+
this.ctx.logger.warn('执行群分析需要提供 channelId 或 guildId。');
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const sendStatus = async (content) => bot?.sendMessage(targetChannel, content, targetGuildContext);
|
|
374
|
+
let message;
|
|
375
|
+
try {
|
|
376
|
+
const cacheKey = (0, utils_1.buildGroupAnalysisCacheKey)(selfId, target, days);
|
|
377
|
+
const cached = this.groupAnalysisCache.get(cacheKey);
|
|
378
|
+
const cacheExpired = (0, utils_1.isCacheExpiredByMinutes)(cached?.analyzedAt, this.config.groupAnalysisCacheMinutes);
|
|
379
|
+
const shouldRefresh = force || cacheExpired || !cached;
|
|
380
|
+
let analysisResult;
|
|
381
|
+
if (!shouldRefresh && cached) {
|
|
382
|
+
analysisResult = cached.result;
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
await sendStatus(`开始分析群聊近 ${days} 天的活动,请稍候...`);
|
|
386
|
+
const messages = await this._getGroupHistoryFromMessageService(selfId, target, days);
|
|
387
|
+
if (messages.length < this.config.minMessages) {
|
|
388
|
+
await sendStatus(`消息数量(${messages.length}/${this.config.minMessages})不足于进行进行有效分析。`);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
this.ctx.logger.info(`群分析已获取 ${messages.length} 条消息,开始智能分析。`);
|
|
392
|
+
analysisResult = await this.analyzeGroupMessages(messages, selfId, target);
|
|
393
|
+
this.groupAnalysisCache.set(cacheKey, {
|
|
394
|
+
result: analysisResult,
|
|
395
|
+
analyzedAt: new Date()
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
const format = outputFormat || this.config.outputFormat || 'image';
|
|
399
|
+
switch (format) {
|
|
400
|
+
case 'image':
|
|
401
|
+
{
|
|
402
|
+
const image = await this.ctx.group_analysis_renderer.renderGroupAnalysis(analysisResult, this.config);
|
|
403
|
+
message =
|
|
404
|
+
typeof image === 'string'
|
|
405
|
+
? koishi_1.h.text(image)
|
|
406
|
+
: koishi_1.h.image(image, 'image/png');
|
|
407
|
+
}
|
|
408
|
+
break;
|
|
409
|
+
case 'pdf': {
|
|
410
|
+
const pdfBuffer = await this.ctx.group_analysis_renderer.renderGroupAnalysisToPdf(analysisResult);
|
|
411
|
+
message = pdfBuffer
|
|
412
|
+
? koishi_1.h.file(pdfBuffer, 'application/pdf')
|
|
413
|
+
: koishi_1.h.text('PDF 渲染失败,请检查日志。');
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
default: {
|
|
417
|
+
message = koishi_1.h.text((0, utils_1.generateTextReport)(analysisResult));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
this.ctx.logger.error(`为群组 ${target.guildId || target.channelId} 执行分析任务时发生错误:`, error);
|
|
423
|
+
const errorMessage = error instanceof Error ? error.message : '未知错误。';
|
|
424
|
+
message = koishi_1.h.text(`分析失败: ${errorMessage}。请检查网络连接和 LLM 配置,或联系管理员。`);
|
|
425
|
+
}
|
|
426
|
+
await bot?.sendMessage(targetChannel, message);
|
|
427
|
+
}
|
|
428
|
+
async executeGroupQuery(session, target, query, outputFormat) {
|
|
429
|
+
const bot = this._getBot(session.selfId);
|
|
430
|
+
const targetChannel = target.channelId ?? target.guildId;
|
|
431
|
+
const targetGuildContext = target.channelId && target.guildId ? target.guildId : undefined;
|
|
432
|
+
if (!targetChannel) {
|
|
433
|
+
this.ctx.logger.warn('执行群分析需要提供 channelId 或 guildId。');
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const sendStatus = async (content) => bot?.sendMessage(targetChannel, content, targetGuildContext);
|
|
437
|
+
const currentTime = new Date();
|
|
438
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
439
|
+
const groupName = await this.resolveGroupName(session.selfId, target);
|
|
440
|
+
this.ctx.logger.info(`收到群分析请求,开始解析用户请求: ${query}`);
|
|
441
|
+
const intent = await this.ctx.group_analysis_llm.parseGroupQuery({
|
|
442
|
+
query,
|
|
443
|
+
currentTime: currentTime.toLocaleString('zh-CN', {
|
|
444
|
+
hour12: false
|
|
445
|
+
}),
|
|
446
|
+
timeZone,
|
|
447
|
+
platform: session.platform,
|
|
448
|
+
groupName,
|
|
449
|
+
guildId: target.guildId,
|
|
450
|
+
channelId: target.channelId,
|
|
451
|
+
currentUserId: session.userId,
|
|
452
|
+
currentUserName: session.username || session.userId
|
|
453
|
+
});
|
|
454
|
+
if (!intent) {
|
|
455
|
+
await sendStatus('未能解析你的请求,请换个说法再试。');
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const action = this.normalizeQueryAction(intent.action);
|
|
459
|
+
const timeRange = this.resolveQueryTimeRange(intent, this.config.cronAnalysisDays || 1);
|
|
460
|
+
this.ctx.logger.info(`准备获取消息记录: ${timeRange.start.toLocaleString()} - ${timeRange.end.toLocaleString()}`);
|
|
461
|
+
const messages = await this._getGroupHistoryFromMessageServiceByTimeRange(session.selfId, target, timeRange.start, timeRange.end);
|
|
462
|
+
if (messages.length < this.config.minMessages) {
|
|
463
|
+
await sendStatus(`消息数量(${messages.length}/${this.config.minMessages})不足于进行有效分析。`);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
this.ctx.logger.info(`群分析已获取 ${messages.length} 条消息,开始智能分析。`);
|
|
467
|
+
const keywords = Array.isArray(intent.keywords)
|
|
468
|
+
? intent.keywords.filter(Boolean)
|
|
469
|
+
: intent.keywords
|
|
470
|
+
? [String(intent.keywords)]
|
|
471
|
+
: [];
|
|
472
|
+
const topics = Array.isArray(intent.topics)
|
|
473
|
+
? intent.topics.filter(Boolean)
|
|
474
|
+
: intent.topics
|
|
475
|
+
? [String(intent.topics)]
|
|
476
|
+
: [];
|
|
477
|
+
const nicknames = Array.isArray(intent.nicknames)
|
|
478
|
+
? intent.nicknames.filter(Boolean)
|
|
479
|
+
: intent.nicknames
|
|
480
|
+
? [String(intent.nicknames)]
|
|
481
|
+
: [];
|
|
482
|
+
const analysisContext = {
|
|
483
|
+
keywords,
|
|
484
|
+
topics,
|
|
485
|
+
nicknames,
|
|
486
|
+
query,
|
|
487
|
+
timeRange
|
|
488
|
+
};
|
|
489
|
+
const analysisResult = await this.analyzeGroupMessages(messages, session.selfId, target, analysisContext);
|
|
490
|
+
const textReport = (0, utils_1.generateTextReport)(analysisResult);
|
|
491
|
+
const format = outputFormat || this.config.outputFormat || 'image';
|
|
492
|
+
if (action !== '只对话') {
|
|
493
|
+
let reportMessage;
|
|
494
|
+
switch (format) {
|
|
495
|
+
case 'image':
|
|
496
|
+
{
|
|
497
|
+
const image = await this.ctx.group_analysis_renderer.renderGroupAnalysis(analysisResult, this.config);
|
|
498
|
+
reportMessage =
|
|
499
|
+
typeof image === 'string'
|
|
500
|
+
? koishi_1.h.text(image)
|
|
501
|
+
: koishi_1.h.image(image, 'image/png');
|
|
502
|
+
}
|
|
503
|
+
break;
|
|
504
|
+
case 'pdf': {
|
|
505
|
+
const pdfBuffer = await this.ctx.group_analysis_renderer.renderGroupAnalysisToPdf(analysisResult);
|
|
506
|
+
reportMessage = pdfBuffer
|
|
507
|
+
? koishi_1.h.file(pdfBuffer, 'application/pdf')
|
|
508
|
+
: koishi_1.h.text('PDF 渲染失败,请检查日志。');
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
default: {
|
|
512
|
+
reportMessage = koishi_1.h.text(textReport);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
await sendStatus(reportMessage);
|
|
516
|
+
}
|
|
517
|
+
if (action !== '只分析') {
|
|
518
|
+
const reply = await this.ctx.group_analysis_llm.replyGroupQuery({
|
|
519
|
+
query,
|
|
520
|
+
analysisResult: textReport,
|
|
521
|
+
currentTime: currentTime.toLocaleString('zh-CN', {
|
|
522
|
+
hour12: false
|
|
523
|
+
}),
|
|
524
|
+
groupName,
|
|
525
|
+
guildId: target.guildId,
|
|
526
|
+
channelId: target.channelId,
|
|
527
|
+
currentUserId: session.userId,
|
|
528
|
+
currentUserName: session.username || session.userId
|
|
529
|
+
});
|
|
530
|
+
if (reply?.length) {
|
|
531
|
+
await sendStatus(reply);
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
await sendStatus('对话生成失败,请稍后再试。');
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
async executeAutoAnalysisForEnabledGroups() {
|
|
539
|
+
const enabledGroups = this.config.listenerGroups.filter((group) => group.enabled);
|
|
540
|
+
const maxConcurrentAnalyses = 3;
|
|
541
|
+
for (let index = 0; index < enabledGroups.length; index += maxConcurrentAnalyses) {
|
|
542
|
+
const currentBatch = enabledGroups.slice(index, index + maxConcurrentAnalyses);
|
|
543
|
+
await Promise.allSettled(currentBatch.map(async (group) => {
|
|
544
|
+
try {
|
|
545
|
+
await this.executeGroupAnalysis(group.selfId, { guildId: group.guildId, channelId: group.channelId }, this.config.cronAnalysisDays);
|
|
546
|
+
}
|
|
547
|
+
catch (err) {
|
|
548
|
+
this.ctx.logger.error(`群 ${group.guildId || group.channelId} 自动分析失败:`, err);
|
|
549
|
+
}
|
|
550
|
+
}));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
async getUserPersona(platform, selfId, userId) {
|
|
554
|
+
const recordId = (0, utils_1.buildPersonaRecordId)(platform, selfId, userId);
|
|
555
|
+
// First, check the cache
|
|
556
|
+
const cached = this.personaCache.get(recordId);
|
|
557
|
+
if (cached?.parsedPersona) {
|
|
558
|
+
return {
|
|
559
|
+
profile: cached.parsedPersona,
|
|
560
|
+
username: cached.record.username
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
// If not in cache, query the database
|
|
564
|
+
const record = await this.ctx.database
|
|
565
|
+
.select('group_analysis_user_personas')
|
|
566
|
+
.where({ id: recordId })
|
|
567
|
+
.execute()
|
|
568
|
+
.then((records) => records[0]);
|
|
569
|
+
if (!record?.persona) {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
try {
|
|
573
|
+
const profile = (0, js_yaml_1.load)(record.persona);
|
|
574
|
+
return { profile, username: record.username };
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
this.ctx.logger.warn(`解析用户画像 YAML 失败 (${recordId}),无法提供画像。`, error);
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async executeUserPersonaAnalysis(session, userId, force) {
|
|
582
|
+
const bot = session.bot;
|
|
583
|
+
await session.send('正在查询用户画像数据,请稍候...');
|
|
584
|
+
let message;
|
|
585
|
+
try {
|
|
586
|
+
const recordId = (0, utils_1.buildPersonaRecordId)(session.platform, session.selfId, userId);
|
|
587
|
+
let avatar;
|
|
588
|
+
let user;
|
|
589
|
+
try {
|
|
590
|
+
user = await bot.getGuildMember(session.channelId || session.guildId, userId);
|
|
591
|
+
}
|
|
592
|
+
catch (error) {
|
|
593
|
+
this.ctx.logger.warn(`获取用户 ${userId} 信息失败: ${error}`);
|
|
594
|
+
}
|
|
595
|
+
const cache = await this.ensurePersonaCache(recordId, {
|
|
596
|
+
platform: session.platform,
|
|
597
|
+
selfId: session.selfId,
|
|
598
|
+
userId,
|
|
599
|
+
username: user?.nick || user?.name || userId
|
|
600
|
+
});
|
|
601
|
+
let displayName = cache.record.username;
|
|
602
|
+
const cacheExpired = (0, utils_1.isCacheExpiredByDays)(cache.record.lastAnalysisAt, this.config.personaCacheLifetimeDays);
|
|
603
|
+
const shouldRefresh = force || cacheExpired || !cache.parsedPersona;
|
|
604
|
+
if (shouldRefresh) {
|
|
605
|
+
if (!force && cache.parsedPersona && cacheExpired) {
|
|
606
|
+
const ttlDays = this.config.personaCacheLifetimeDays;
|
|
607
|
+
if (ttlDays > 0) {
|
|
608
|
+
await session.send(`上次用户画像更新已超过 ${ttlDays} 天,正在重新生成画像。`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const sourceGroup = {
|
|
612
|
+
guildId: session.guildId || undefined,
|
|
613
|
+
channelId: session.channelId || undefined,
|
|
614
|
+
platform: session.platform,
|
|
615
|
+
selfId: session.selfId
|
|
616
|
+
};
|
|
617
|
+
await this.runPersonaAnalysis(cache, sourceGroup);
|
|
618
|
+
if (!cache.parsedPersona) {
|
|
619
|
+
message = koishi_1.h.text('暂未收集到足够的聊天记录来生成该用户的画像,请稍后再试。');
|
|
620
|
+
await session.send(message);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
cache.pendingMessages = 0;
|
|
624
|
+
}
|
|
625
|
+
const profile = cache.parsedPersona;
|
|
626
|
+
if (user) {
|
|
627
|
+
avatar = user.avatar;
|
|
628
|
+
const resolvedName = user.nick ||
|
|
629
|
+
user.name;
|
|
630
|
+
if (resolvedName) {
|
|
631
|
+
displayName = resolvedName;
|
|
632
|
+
cache.record.username = resolvedName;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (session.platform === 'onebot') {
|
|
636
|
+
avatar = (0, utils_1.getAvatarUrl)(userId);
|
|
637
|
+
}
|
|
638
|
+
profile.analysisDate = cache.record.lastAnalysisAt.toLocaleString();
|
|
639
|
+
const image = await this.ctx.group_analysis_renderer.renderUserPersona(profile, displayName, avatar, this.config);
|
|
640
|
+
message =
|
|
641
|
+
typeof image === 'string'
|
|
642
|
+
? koishi_1.h.text(image)
|
|
643
|
+
: koishi_1.h.image(image, 'image/png');
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
this.ctx.logger.error(`为用户 ${userId} 执行画像分析时发生错误:`, error);
|
|
647
|
+
const errorMessage = error instanceof Error ? error.message : '未知错误。';
|
|
648
|
+
message = koishi_1.h.text(`分析失败: ${errorMessage}。请检查服务状态或联系管理员。`);
|
|
649
|
+
}
|
|
650
|
+
await session.send(message);
|
|
651
|
+
}
|
|
652
|
+
async analyzeGroupMessages(messages, selfId, target, context) {
|
|
653
|
+
this.ctx.logger.info(`开始分析 ${messages.length} 条消息...`);
|
|
654
|
+
const { userStats, totalChars, totalEmojiCount, allMessagesText } = (0, utils_1.calculateBasicStats)(messages);
|
|
655
|
+
const messagesText = allMessagesText.join('\n');
|
|
656
|
+
// LLM analyses in parallel
|
|
657
|
+
const users = Object.values(userStats);
|
|
658
|
+
const [topics, userTitles, goldenQuotes] = await Promise.all([
|
|
659
|
+
this.ctx.group_analysis_llm.summarizeTopics(messagesText, context),
|
|
660
|
+
this.config.userTitleAnalysis
|
|
661
|
+
? this.ctx.group_analysis_llm.analyzeUserTitles(users, context)
|
|
662
|
+
: Promise.resolve([]),
|
|
663
|
+
this.ctx.group_analysis_llm.analyzeGoldenQuotes(messagesText, this.config.maxGoldenQuotes, context)
|
|
664
|
+
]).catch((error) => {
|
|
665
|
+
this.ctx.logger.error('LLM analysis failed:', error);
|
|
666
|
+
// On LLM failure, return empty results to avoid crashing the entire analysis.
|
|
667
|
+
return [
|
|
668
|
+
[],
|
|
669
|
+
[],
|
|
670
|
+
[]
|
|
671
|
+
];
|
|
672
|
+
});
|
|
673
|
+
// Final statistics
|
|
674
|
+
const sortedUsers = users.sort((a, b) => b.messageCount - a.messageCount);
|
|
675
|
+
const overallActiveHours = users.reduce((acc, user) => {
|
|
676
|
+
for (const hour in user.activeHours) {
|
|
677
|
+
acc[hour] = (acc[hour] || 0) + user.activeHours[hour];
|
|
678
|
+
}
|
|
679
|
+
return acc;
|
|
680
|
+
}, {});
|
|
681
|
+
const mostActiveHourEntry = Object.entries(overallActiveHours).sort((a, b) => b[1] - a[1])[0];
|
|
682
|
+
const mostActiveHour = mostActiveHourEntry
|
|
683
|
+
? mostActiveHourEntry[0]
|
|
684
|
+
: 'N/A';
|
|
685
|
+
// Generate chart using the renderer service
|
|
686
|
+
const activeHoursChartHtml = (0, utils_1.generateActiveHoursChart)(overallActiveHours);
|
|
687
|
+
const groupName = await this.resolveGroupName(selfId, target);
|
|
688
|
+
const result = {
|
|
689
|
+
totalMessages: messages.length,
|
|
690
|
+
totalChars,
|
|
691
|
+
totalParticipants: users.length,
|
|
692
|
+
emojiCount: totalEmojiCount,
|
|
693
|
+
mostActiveUser: sortedUsers[0] || null,
|
|
694
|
+
mostActivePeriod: mostActiveHour !== 'N/A'
|
|
695
|
+
? `${mostActiveHour.padStart(2, '0')}:00 - ${String(parseInt(mostActiveHour) + 1).padStart(2, '0')}:00`
|
|
696
|
+
: 'N/A',
|
|
697
|
+
userStats: sortedUsers.slice(0, this.config.maxUsersInReport),
|
|
698
|
+
topics,
|
|
699
|
+
userTitles,
|
|
700
|
+
goldenQuotes,
|
|
701
|
+
activeHoursChart: activeHoursChartHtml,
|
|
702
|
+
activeHoursData: overallActiveHours,
|
|
703
|
+
analysisDate: new Date().toLocaleDateString('zh-CN'),
|
|
704
|
+
groupName
|
|
705
|
+
};
|
|
706
|
+
this.ctx.logger.info('消息分析完成。');
|
|
707
|
+
return result;
|
|
708
|
+
}
|
|
709
|
+
_getBot(selfId) {
|
|
710
|
+
return this.ctx.bots.find((bot) => bot.selfId === selfId);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
exports.AnalysisService = AnalysisService;
|
|
714
|
+
//# sourceMappingURL=analysis.js.map
|