koishi-plugin-group-analysis-mx 1.0.16 → 1.1.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 CHANGED
@@ -116,6 +116,54 @@ exports.Config = koishi_1.Schema.intersect([
116
116
  .max(2)
117
117
  .default(1.5)
118
118
  }).description('OpenAI API 设置'),
119
+ koishi_1.Schema.object({
120
+ enableDailyComic: koishi_1.Schema.boolean()
121
+ .description('是否启用每日漫画功能。')
122
+ .default(false),
123
+ comicSchedule: koishi_1.Schema.string()
124
+ .description('每日漫画自动生成的 CRON 表达式。留空则仅手动触发。例如 "0 20 * * *" 表示每天 20:00。')
125
+ .default(''),
126
+ comicDays: koishi_1.Schema.number()
127
+ .description('漫画话题分析回溯的天数。')
128
+ .min(1)
129
+ .max(7)
130
+ .default(1),
131
+ comicMinMessages: koishi_1.Schema.number()
132
+ .description('生成漫画所需的最小消息数量。')
133
+ .min(10)
134
+ .default(50),
135
+ promptComicStoryboard: koishi_1.Schema.string()
136
+ .description('漫画分镜提示词模板。')
137
+ .role('textarea')
138
+ .default(`你是一个资深的漫画分镜师与 AI 绘画提示词专家。请根据以下【群聊每日核心话题列表】,将其改编并设计为一个精彩的多格连环漫画(Comic Strip)全景视觉画图提示词 (Prompt)。
139
+
140
+ 【核心规则】:
141
+ 1. 每个话题对应一个分格 Panel(共 {topicCount} 个分格),不许遗漏任何话题。
142
+ 2. 为每个话题设计一句生动的人物台词或吐槽(控制在 15 个汉字以内),放在角色气泡中(adorable kawaii anime speech bubble, soft rounded cloud-like shape, cute pointer tail pointing to the speaker)。
143
+ 3. 每个分格底部设计一条精炼的事件旁白条(控制在 30 字以内,cute pastel-colored kawaii caption strip at the bottom of the panel with soft rounded corners,严禁死板白框)。
144
+ 4. 气泡与旁白条内的中文必须显式渲染(指令格式:containing a speech bubble with exact Chinese text "台词" 以及 and a cute caption strip at bottom with exact Chinese text "短标题"),绝对禁止将中文翻译成英文。
145
+ 5. 画面整体构图与场景描述使用英文;严禁将长篇上下文写在画面上,仅允许渲染短标题字幕条和气泡台词。
146
+ 6. 保持整篇连环画的主角(一个可爱的动漫少女角色)绝对连贯,每个分格都必须出现。
147
+
148
+ 【待创作的群聊核心话题列表】:
149
+ {topics}
150
+
151
+ 请输出包含 "scene" 字段的 JSON 对象,scene 为完整的英文生图提示词。`)
152
+ }).description('每日漫画设置'),
153
+ koishi_1.Schema.object({
154
+ drawingEndpoint: koishi_1.Schema.string()
155
+ .description('OpenAI 兼容的生图 API 地址(末尾无需 /v1)。支持 OpenAI、SiliconFlow、豆包/火山方舟等。')
156
+ .default(''),
157
+ drawingApiKey: koishi_1.Schema.string()
158
+ .role('secret')
159
+ .description('生图 API Key。'),
160
+ drawingModel: koishi_1.Schema.string()
161
+ .description('生图模型名称,例如 gpt-image-1、dall-e-3、Kolors 等。')
162
+ .default('gpt-image-1'),
163
+ drawingSize: koishi_1.Schema.string()
164
+ .description('生图尺寸。')
165
+ .default('1024x1024')
166
+ }).description('生图 API 设置'),
119
167
  koishi_1.Schema.object({
120
168
  personaUserFilter: koishi_1.Schema.array(String)
121
169
  .role('table')
package/lib/index.js CHANGED
@@ -18,6 +18,7 @@ exports.apply = apply;
18
18
  const analysis_1 = require("./service/analysis");
19
19
  const llm_1 = require("./service/llm");
20
20
  const renderer_1 = require("./service/renderer");
21
+ const drawing_1 = require("./service/drawing");
21
22
  const message_1 = require("./service/message");
22
23
  const plugin_1 = require("./plugin");
23
24
  const cron_1 = require("./cron");
@@ -28,15 +29,18 @@ function apply(ctx, config) {
28
29
  ctx.plugin(llm_1.LLMService, config);
29
30
  ctx.plugin(analysis_1.AnalysisService, config);
30
31
  ctx.plugin(renderer_1.RendererService, config);
32
+ ctx.plugin(drawing_1.DrawingService, config);
31
33
  ctx.inject([
32
34
  'group_analysis_message',
33
35
  'group_analysis_llm',
34
- 'group_analysis_renderer'
36
+ 'group_analysis_renderer',
37
+ 'group_analysis_drawing'
35
38
  ], (ctx) => {
36
39
  (0, plugin_1.plugin)(ctx, config);
37
40
  });
38
41
  ctx.inject(['group_analysis'], (ctx) => {
39
42
  ctx.effect(() => scheduleAutoAnalysis(ctx, config));
43
+ ctx.effect(() => scheduleAutoComic(ctx, config));
40
44
  });
41
45
  }
42
46
  function scheduleAutoAnalysis(ctx, config) {
@@ -48,4 +52,13 @@ function scheduleAutoAnalysis(ctx, config) {
48
52
  name: 'group-analysis'
49
53
  });
50
54
  }
55
+ function scheduleAutoComic(ctx, config) {
56
+ if (!config.enableDailyComic || !config.comicSchedule?.trim()) {
57
+ return () => { };
58
+ }
59
+ return (0, cron_1.cron)(ctx, config.comicSchedule, () => ctx.group_analysis.executeDailyComicForEnabledGroups(), {
60
+ cooldown: 30,
61
+ name: 'group-comic'
62
+ });
63
+ }
51
64
  //# sourceMappingURL=index.js.map
@@ -208,5 +208,41 @@ function apply(ctx, config) {
208
208
  return '用户画像分析执行失败,请检查日志。';
209
209
  }
210
210
  });
211
+ ctx.command('每日漫画 [days:number]', '生成今日群聊漫画')
212
+ .alias('daily-comic')
213
+ .alias('群分析.每日漫画')
214
+ .usage('使用方法:/每日漫画 或 /每日漫画 3(回溯 3 天)。需要已配置生图 API。')
215
+ .option('group', '-g <guildId:string> 指定群号', {
216
+ authority: 3
217
+ })
218
+ .option('channel', '-c <channelId:string> 指定频道号', {
219
+ authority: 3
220
+ })
221
+ .action(async ({ session, options }, days) => {
222
+ if (config.personaBlacklist.includes(session.userId)) {
223
+ return '您无权使用本插件。';
224
+ }
225
+ if (session.isDirect && !options.group && !options.channel) {
226
+ return '私聊中请使用 -g 或 -c 指定目标群或频道。';
227
+ }
228
+ const targetGuildId = options.group ?? session.guildId ?? undefined;
229
+ const targetChannelId = options.channel ?? session.channelId ?? undefined;
230
+ if (!targetGuildId && !targetChannelId) {
231
+ return '请使用 -g 或 -c 指定目标群或频道。';
232
+ }
233
+ if (!checkGroup(session, {
234
+ guildId: targetGuildId,
235
+ channelId: targetChannelId
236
+ })) {
237
+ return '目标群未启用分析功能,请使用 群分析.启用 来启用目标群的分析功能。';
238
+ }
239
+ const targetDays = typeof days === 'number' && days >= 1
240
+ ? Math.min(days, 7)
241
+ : undefined;
242
+ await ctx.group_analysis.executeDailyComic(session.selfId, {
243
+ guildId: targetGuildId || undefined,
244
+ channelId: targetChannelId || undefined
245
+ }, targetDays);
246
+ });
211
247
  }
212
248
  //# sourceMappingURL=command.js.map
@@ -9,7 +9,8 @@ class AnalysisService extends koishi_1.Service {
9
9
  static inject = [
10
10
  'group_analysis_llm',
11
11
  'group_analysis_message',
12
- 'group_analysis_renderer'
12
+ 'group_analysis_renderer',
13
+ 'group_analysis_drawing'
13
14
  ];
14
15
  personaCache = new Map();
15
16
  personaProcessing = new Set();
@@ -163,21 +164,28 @@ class AnalysisService extends koishi_1.Service {
163
164
  const results = [];
164
165
  const useOnlyCurrentGroup = this.config.personaOnlyCurrentGroup ||
165
166
  this.config.enableAllGroupsByDefault;
166
- const relevantGroups = useOnlyCurrentGroup
167
- ? [
168
- {
169
- enabled: true,
170
- platform: sourceGroup?.platform,
171
- selfId: sourceGroup?.selfId,
172
- channelId: sourceGroup?.channelId,
173
- guildId: sourceGroup?.guildId
174
- }
175
- ].filter((group) => !!group.platform &&
176
- !!group.selfId &&
177
- (!!group.channelId || !!group.guildId))
167
+ // 优先匹配配置的监听群;未匹配到时回退到触发会话的当前群,
168
+ // 保证官方 QQ 机器人等平台下画像分析仍能收集到消息。
169
+ let relevantGroups = useOnlyCurrentGroup
170
+ ? []
178
171
  : this.config.listenerGroups.filter((group) => group.enabled &&
179
172
  group.platform === record.platform &&
180
173
  group.selfId === record.selfId);
174
+ if (!relevantGroups.length && sourceGroup) {
175
+ const fallbackGroup = {
176
+ enabled: true,
177
+ platform: sourceGroup.platform,
178
+ selfId: sourceGroup.selfId,
179
+ channelId: sourceGroup.channelId,
180
+ guildId: sourceGroup.guildId
181
+ };
182
+ if (fallbackGroup.platform &&
183
+ fallbackGroup.selfId &&
184
+ (fallbackGroup.channelId || fallbackGroup.guildId)) {
185
+ this.ctx.logger.info(`用户 ${record.userId} 画像分析未匹配到配置监听群,回退使用当前群 (${fallbackGroup.channelId || fallbackGroup.guildId})。`);
186
+ relevantGroups = [fallbackGroup];
187
+ }
188
+ }
181
189
  if (!relevantGroups.length) {
182
190
  const reason = useOnlyCurrentGroup
183
191
  ? '未提供可用的当前群组信息'
@@ -185,6 +193,7 @@ class AnalysisService extends koishi_1.Service {
185
193
  this.ctx.logger.warn(`用户 ${record.userId} 画像分析缺少群组上下文(${reason}),跳过画像分析。`);
186
194
  return [];
187
195
  }
196
+ this.ctx.logger.info(`用户 ${record.userId} 画像分析将从 ${relevantGroups.length} 个群收集消息。`);
188
197
  const totalLimit = this.config.personaMaxMessages;
189
198
  for (const group of relevantGroups) {
190
199
  const bot = this._getBot(group.selfId);
@@ -193,18 +202,18 @@ class AnalysisService extends koishi_1.Service {
193
202
  if (bot.platform === 'onebot') {
194
203
  userGroupInfo = await bot.internal.getGroupMemberInfo(group.guildId, record.userId, true);
195
204
  }
196
- else {
205
+ else if (bot?.getGuildMember) {
197
206
  userGroupInfo = await bot.getGuildMember(group.channelId || group.guildId, record.userId);
198
207
  }
199
- if (userGroupInfo == null) {
200
- continue;
201
- }
202
208
  }
203
209
  catch (error) {
204
- this.ctx.logger.warn(`获取用户 ${record.userId} 的群组信息失败 (${group.channelId || group.guildId}),可能是未加入该群聊。将跳过此群组的信息获取。`, error);
205
- continue;
210
+ // 官方 QQ 机器人等平台可能没有获取群成员信息的权限,
211
+ // 不中断画像分析,仅跳过角色信息(roles 保持为空)。
212
+ this.ctx.logger.debug(`获取用户 ${record.userId} 的群组信息失败 (${group.channelId || group.guildId}),将跳过角色信息: ${error}`);
213
+ }
214
+ if (userGroupInfo) {
215
+ record.roles = (userGroupInfo.roles || []).map((r) => (typeof r === 'string' ? r : r.name || r.id || ''));
206
216
  }
207
- record.roles = (userGroupInfo.roles || []).map((r) => (typeof r === 'string' ? r : r.name || r.id || ''));
208
217
  const history = await this.ctx.group_analysis_message.getHistoricalMessages({
209
218
  guildId: group.guildId,
210
219
  channelId: group.channelId,
@@ -614,13 +623,14 @@ class AnalysisService extends koishi_1.Service {
614
623
  user = await bot.getGuildMember(session.channelId || session.guildId, userId);
615
624
  }
616
625
  catch (error) {
617
- this.ctx.logger.warn(`获取用户 ${userId} 信息失败: ${error}`);
626
+ // 官方 QQ 机器人等平台可能没有获取群成员信息的权限,回退到本地记录
627
+ this.ctx.logger.debug(`获取用户 ${userId} 的成员信息失败,将回退到本地记录: ${error}`);
618
628
  }
619
629
  const cache = await this.ensurePersonaCache(recordId, {
620
630
  platform: session.platform,
621
631
  selfId: session.selfId,
622
632
  userId,
623
- username: user?.nick || user?.name || userId
633
+ username: user?.nick || user?.name || user?.user?.name || userId
624
634
  });
625
635
  let displayName = cache.record.username;
626
636
  const cacheExpired = (0, utils_1.isCacheExpiredByDays)(cache.record.lastAnalysisAt, this.config.personaCacheLifetimeDays);
@@ -648,17 +658,36 @@ class AnalysisService extends koishi_1.Service {
648
658
  }
649
659
  const profile = cache.parsedPersona;
650
660
  if (user) {
651
- avatar = user.avatar;
661
+ // Koishi Universal.GuildMember 结构:{ user: { id, name, avatar }, name, ... }
662
+ avatar = user.avatar || user.user?.avatar;
652
663
  const resolvedName = user.nick ||
653
- user.name;
664
+ user.name ||
665
+ user.user?.name;
654
666
  if (resolvedName) {
655
667
  displayName = resolvedName;
656
668
  cache.record.username = resolvedName;
657
669
  }
658
670
  }
671
+ // 平台化头像解析:onebot 走 QQ 号接口,官方 QQ 机器人走 openid 模板
659
672
  if (session.platform === 'onebot') {
660
673
  avatar = (0, utils_1.getAvatarUrl)(userId);
661
674
  }
675
+ else if (!avatar) {
676
+ avatar = (0, utils_1.resolveUserAvatar)(bot, userId);
677
+ }
678
+ // 昵称/头像兜底:从数据库已记录的消息中回退用户资料
679
+ if (!displayName || displayName === userId || !avatar) {
680
+ const fallback = await this.lookupUserProfileFromMessages(session.platform, session.selfId, userId);
681
+ if (fallback) {
682
+ if ((!displayName || displayName === userId) && fallback.username) {
683
+ displayName = fallback.username;
684
+ cache.record.username = fallback.username;
685
+ }
686
+ if (!avatar && fallback.avatarUrl) {
687
+ avatar = fallback.avatarUrl;
688
+ }
689
+ }
690
+ }
662
691
  profile.analysisDate = cache.record.lastAnalysisAt.toLocaleString();
663
692
  const image = await this.ctx.group_analysis_renderer.renderUserPersona(profile, displayName, avatar, this.config);
664
693
  message =
@@ -673,6 +702,87 @@ class AnalysisService extends koishi_1.Service {
673
702
  }
674
703
  await session.send(message);
675
704
  }
705
+ async lookupUserProfileFromMessages(platform, selfId, userId) {
706
+ try {
707
+ const records = await this.ctx.database
708
+ .select('group_analysis_messages')
709
+ .where({ platform, selfId, userId })
710
+ .orderBy(($) => $.timestamp, 'desc')
711
+ .limit(1)
712
+ .execute();
713
+ const latest = records[0];
714
+ if (!latest)
715
+ return null;
716
+ return {
717
+ username: latest.username || null,
718
+ avatarUrl: latest.avatarUrl || null
719
+ };
720
+ }
721
+ catch (error) {
722
+ this.ctx.logger.debug(`从消息记录回退用户资料失败 (${platform}/${userId}): ${error}`);
723
+ return null;
724
+ }
725
+ }
726
+ async executeDailyComic(selfId, target, days) {
727
+ const bot = this._getBot(selfId);
728
+ const targetChannel = target.channelId ?? target.guildId;
729
+ if (!targetChannel) {
730
+ this.ctx.logger.warn('执行每日漫画需要提供 channelId 或 guildId。');
731
+ return;
732
+ }
733
+ const sendStatus = async (content) => bot?.sendMessage(targetChannel, content, target.guildId);
734
+ const lookbackDays = days ?? this.config.comicDays ?? 1;
735
+ try {
736
+ await sendStatus('正在生成今日群聊漫画,请稍候...');
737
+ const messages = await this._getGroupHistoryFromMessageService(selfId, target, lookbackDays);
738
+ if (messages.length < this.config.comicMinMessages) {
739
+ await sendStatus(`消息数量(${messages.length}/${this.config.comicMinMessages})不足以生成漫画,请稍后再试。`);
740
+ return;
741
+ }
742
+ // 1. 提取话题
743
+ const { allMessagesText } = (0, utils_1.calculateBasicStats)(messages);
744
+ const messagesText = allMessagesText.join('\n');
745
+ const topics = await this.ctx.group_analysis_llm.summarizeTopics(messagesText, null);
746
+ if (!topics?.length) {
747
+ await sendStatus('未能从群聊记录中提取到有效话题,无法生成漫画。');
748
+ return;
749
+ }
750
+ // 2. 生成漫画分镜提示词
751
+ const scene = await this.ctx.group_analysis_llm.analyzeComicStoryboards(topics);
752
+ if (!scene) {
753
+ await sendStatus('漫画分镜提示词生成失败,请检查日志。');
754
+ return;
755
+ }
756
+ // 3. 调用生图 API 生成漫画
757
+ const imageBuffer = await this.ctx.group_analysis_drawing.generateImage(scene);
758
+ const mime = imageBuffer[0] === 0x89
759
+ ? 'image/png'
760
+ : imageBuffer[0] === 0xff
761
+ ? 'image/jpeg'
762
+ : 'image/png';
763
+ await bot?.sendMessage(targetChannel, koishi_1.h.image(imageBuffer, mime), target.guildId);
764
+ this.ctx.logger.info(`每日漫画生成并发送成功 (${target.guildId || target.channelId}),图片大小: ${imageBuffer.length} bytes。`);
765
+ }
766
+ catch (error) {
767
+ this.ctx.logger.error(`为群组 ${target.guildId || target.channelId} 生成每日漫画失败:`, error);
768
+ const errorMessage = error instanceof Error ? error.message : '未知错误。';
769
+ await sendStatus(`漫画生成失败: ${errorMessage}`);
770
+ }
771
+ }
772
+ async executeDailyComicForEnabledGroups() {
773
+ const enabledGroups = this.config.listenerGroups.filter((group) => group.enabled);
774
+ for (const group of enabledGroups) {
775
+ try {
776
+ await this.executeDailyComic(group.selfId, {
777
+ guildId: group.guildId,
778
+ channelId: group.channelId
779
+ });
780
+ }
781
+ catch (err) {
782
+ this.ctx.logger.error(`群 ${group.guildId || group.channelId} 自动漫画生成失败:`, err);
783
+ }
784
+ }
785
+ }
676
786
  async analyzeGroupMessages(messages, selfId, target, context) {
677
787
  this.ctx.logger.info(`开始分析 ${messages.length} 条消息...`);
678
788
  const { userStats, totalChars, totalEmojiCount, allMessagesText } = (0, utils_1.calculateBasicStats)(messages);
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DrawingService = void 0;
4
+ const koishi_1 = require("koishi");
5
+ class DrawingService extends koishi_1.Service {
6
+ config;
7
+ constructor(ctx, config) {
8
+ super(ctx, 'group_analysis_drawing', true);
9
+ this.config = config;
10
+ }
11
+ /**
12
+ * 调用 OpenAI 兼容的图片生成 API(/images/generations)生成一张图片。
13
+ * @param prompt 生图提示词
14
+ * @returns 图片字节(PNG/JPEG);失败时抛出异常
15
+ */
16
+ async generateImage(prompt) {
17
+ const endpoint = (this.config.drawingEndpoint || '').replace(/\/$/, '');
18
+ if (!endpoint || !this.config.drawingApiKey) {
19
+ throw new Error('未配置生图 API(drawingEndpoint / drawingApiKey)。请在插件配置中填写。');
20
+ }
21
+ const url = `${endpoint}/images/generations`;
22
+ this.ctx.logger.info('正在调用生图 API 生成漫画...');
23
+ let response;
24
+ try {
25
+ response = await this.ctx.http.post(url, {
26
+ model: this.config.drawingModel || 'gpt-image-1',
27
+ prompt,
28
+ n: 1,
29
+ size: this.config.drawingSize || '1024x1024'
30
+ }, {
31
+ headers: {
32
+ 'Authorization': `Bearer ${this.config.drawingApiKey}`,
33
+ 'Content-Type': 'application/json'
34
+ }
35
+ });
36
+ }
37
+ catch (error) {
38
+ this.ctx.logger.error('生图 API 调用失败:', error);
39
+ throw new Error(`生图 API 调用失败: ${error instanceof Error ? error.message : error}`);
40
+ }
41
+ const item = response?.data?.[0];
42
+ if (!item) {
43
+ this.ctx.logger.error(`生图 API 返回空响应: ${JSON.stringify(response)}`);
44
+ throw new Error('生图 API 返回空响应。');
45
+ }
46
+ if (item.b64_json) {
47
+ this.ctx.logger.info('生图 API 返回 base64 图片。');
48
+ return Buffer.from(item.b64_json, 'base64');
49
+ }
50
+ if (item.url) {
51
+ this.ctx.logger.info(`生图 API 返回图片 URL,开始下载: ${item.url}`);
52
+ const imageResponse = await fetch(item.url);
53
+ if (!imageResponse.ok) {
54
+ throw new Error(`下载生图结果失败: ${imageResponse.status} ${imageResponse.statusText}`);
55
+ }
56
+ const arrayBuffer = await imageResponse.arrayBuffer();
57
+ return Buffer.from(arrayBuffer);
58
+ }
59
+ throw new Error('生图 API 响应中既无 b64_json 也无 url。');
60
+ }
61
+ }
62
+ exports.DrawingService = DrawingService;
@@ -155,6 +155,41 @@ class LLMService extends koishi_1.Service {
155
155
  .replace('{analysisResult}', promptContext.analysisResult || '');
156
156
  return this._callText(prompt, '群分析对话回复');
157
157
  }
158
+ async analyzeComicStoryboards(topics) {
159
+ const validTopics = (topics || []).filter((t) => t?.topic);
160
+ const topicsText = validTopics
161
+ .map((t, i) => `${i + 1}. 话题: ${t.topic}\n 详情: ${t.detail || ''}`)
162
+ .join('\n');
163
+ const prompt = this.config.promptComicStoryboard
164
+ .replace('{topics}', topicsText || '(无话题)')
165
+ .replace('{topicCount}', String(Math.max(validTopics.length, 1)));
166
+ this.ctx.logger.info(`正在生成漫画分镜提示词(${validTopics.length} 个话题)...`);
167
+ const rawContent = await this._callOpenAI(prompt, '漫画分镜提示词');
168
+ // 优先解析 JSON(兼容 ```json 代码块),失败时用正则提取 scene 字段
169
+ const jsonMatch = rawContent.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
170
+ const candidates = [jsonMatch?.[1], rawContent].filter(Boolean);
171
+ for (const candidate of candidates) {
172
+ try {
173
+ const data = JSON.parse(candidate);
174
+ const scene = data?.scene ||
175
+ (Array.isArray(data?.storyboards)
176
+ ? data.storyboards.map((s) => s?.scene).filter(Boolean).join('\n\n')
177
+ : null);
178
+ if (typeof scene === 'string' && scene.trim()) {
179
+ return scene.trim();
180
+ }
181
+ }
182
+ catch (err) {
183
+ // 继续尝试下一个候选
184
+ }
185
+ }
186
+ const sceneMatch = rawContent.match(/"scene"\s*:\s*"((?:[^"\\]|\\.)*)"/);
187
+ if (sceneMatch) {
188
+ return sceneMatch[1].replace(/\\"/g, '"').trim();
189
+ }
190
+ this.ctx.logger.warn('未能从 LLM 响应中解析出漫画 scene 提示词。');
191
+ return null;
192
+ }
158
193
  }
159
194
  exports.LLMService = LLMService;
160
195
  //# sourceMappingURL=llm.js.map
@@ -52,12 +52,17 @@ class MessageService extends koishi_1.Service {
52
52
  }
53
53
  shouldListenToMessage(session) {
54
54
  // TODO: private message
55
- return (0, utils_1.shouldListenToMessage)({
55
+ const result = (0, utils_1.shouldListenToMessage)({
56
56
  guildId: session.guildId || undefined,
57
57
  channelId: session.channelId || undefined,
58
58
  platform: session.platform,
59
59
  selfId: session.selfId
60
60
  }, this.config.listenerGroups, this.config.enableAllGroupsByDefault);
61
+ if (!result) {
62
+ // 帮助定位"消息未被记录/画像消息为 0"的问题
63
+ this.ctx.logger.debug(`消息未计入统计 (platform=${session.platform}, selfId=${session.selfId}, guildId=${session.guildId}, channelId=${session.channelId}),请检查"监听群组"配置(需与机器人实际平台/ID 一致)。`);
64
+ }
65
+ return result;
61
66
  }
62
67
  async handleMessage(session) {
63
68
  // 黑名单用户的消息完全不记录、不参与统计
package/lib/utils.js CHANGED
@@ -4,6 +4,7 @@ exports.calculateBasicStats = calculateBasicStats;
4
4
  exports.buildPersonaRecordId = buildPersonaRecordId;
5
5
  exports.generateTextReport = generateTextReport;
6
6
  exports.getAvatarUrl = getAvatarUrl;
7
+ exports.resolveUserAvatar = resolveUserAvatar;
7
8
  exports.formatUserStats = formatUserStats;
8
9
  exports.formatGoldenQuotes = formatGoldenQuotes;
9
10
  exports.formatUserTitles = formatUserTitles;
@@ -154,6 +155,27 @@ function generateTextReport(result) {
154
155
  function getAvatarUrl(userId) {
155
156
  return `http://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`;
156
157
  }
158
+ /**
159
+ * 按平台解析用户头像 URL。
160
+ * - onebot: 使用 QQ 号头像接口(q1.qlogo.cn)
161
+ * - qq(官方 QQ 机器人): 使用 openid 头像模板 q.qlogo.cn/qqapp/{appid}/{openid}/640
162
+ * - 其他平台: 返回 null,由调用方回退到事件头像或默认头像
163
+ */
164
+ function resolveUserAvatar(bot, userId) {
165
+ if (!bot || !userId)
166
+ return null;
167
+ if (bot.platform === 'onebot') {
168
+ return getAvatarUrl(userId);
169
+ }
170
+ if (bot.platform === 'qq') {
171
+ const appid = bot.config?.id;
172
+ if (appid) {
173
+ return `https://q.qlogo.cn/qqapp/${appid}/${userId}/640`;
174
+ }
175
+ return null;
176
+ }
177
+ return null;
178
+ }
157
179
  function formatUserStats(userStats, skin = 'md3') {
158
180
  const renderer = skins_1.skinRegistry.getSafe(skin);
159
181
  return renderer.formatUserStats(userStats);
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.0.16",
4
+ "version": "1.1.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/index.mjs",
7
7
  "typings": "lib/index.d.ts",