koishi-plugin-group-analysis-mx 1.0.17 → 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 +57 -1
- package/lib/index.js +14 -1
- package/lib/plugins/command.js +36 -0
- package/lib/service/analysis.js +100 -13
- package/lib/service/drawing.js +62 -0
- package/lib/service/llm.js +35 -0
- package/lib/service/message.js +6 -1
- 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')
|
|
@@ -116,6 +124,54 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
116
124
|
.max(2)
|
|
117
125
|
.default(1.5)
|
|
118
126
|
}).description('OpenAI API 设置'),
|
|
127
|
+
koishi_1.Schema.object({
|
|
128
|
+
enableDailyComic: koishi_1.Schema.boolean()
|
|
129
|
+
.description('是否启用每日漫画功能。')
|
|
130
|
+
.default(false),
|
|
131
|
+
comicSchedule: koishi_1.Schema.string()
|
|
132
|
+
.description('每日漫画自动生成的 CRON 表达式。留空则仅手动触发。例如 "0 20 * * *" 表示每天 20:00。')
|
|
133
|
+
.default(''),
|
|
134
|
+
comicDays: koishi_1.Schema.number()
|
|
135
|
+
.description('漫画话题分析回溯的天数。')
|
|
136
|
+
.min(1)
|
|
137
|
+
.max(7)
|
|
138
|
+
.default(1),
|
|
139
|
+
comicMinMessages: koishi_1.Schema.number()
|
|
140
|
+
.description('生成漫画所需的最小消息数量。')
|
|
141
|
+
.min(10)
|
|
142
|
+
.default(50),
|
|
143
|
+
promptComicStoryboard: koishi_1.Schema.string()
|
|
144
|
+
.description('漫画分镜提示词模板。')
|
|
145
|
+
.role('textarea')
|
|
146
|
+
.default(`你是一个资深的漫画分镜师与 AI 绘画提示词专家。请根据以下【群聊每日核心话题列表】,将其改编并设计为一个精彩的多格连环漫画(Comic Strip)全景视觉画图提示词 (Prompt)。
|
|
147
|
+
|
|
148
|
+
【核心规则】:
|
|
149
|
+
1. 每个话题对应一个分格 Panel(共 {topicCount} 个分格),不许遗漏任何话题。
|
|
150
|
+
2. 为每个话题设计一句生动的人物台词或吐槽(控制在 15 个汉字以内),放在角色气泡中(adorable kawaii anime speech bubble, soft rounded cloud-like shape, cute pointer tail pointing to the speaker)。
|
|
151
|
+
3. 每个分格底部设计一条精炼的事件旁白条(控制在 30 字以内,cute pastel-colored kawaii caption strip at the bottom of the panel with soft rounded corners,严禁死板白框)。
|
|
152
|
+
4. 气泡与旁白条内的中文必须显式渲染(指令格式:containing a speech bubble with exact Chinese text "台词" 以及 and a cute caption strip at bottom with exact Chinese text "短标题"),绝对禁止将中文翻译成英文。
|
|
153
|
+
5. 画面整体构图与场景描述使用英文;严禁将长篇上下文写在画面上,仅允许渲染短标题字幕条和气泡台词。
|
|
154
|
+
6. 保持整篇连环画的主角(一个可爱的动漫少女角色)绝对连贯,每个分格都必须出现。
|
|
155
|
+
|
|
156
|
+
【待创作的群聊核心话题列表】:
|
|
157
|
+
{topics}
|
|
158
|
+
|
|
159
|
+
请输出包含 "scene" 字段的 JSON 对象,scene 为完整的英文生图提示词。`)
|
|
160
|
+
}).description('每日漫画设置'),
|
|
161
|
+
koishi_1.Schema.object({
|
|
162
|
+
drawingEndpoint: koishi_1.Schema.string()
|
|
163
|
+
.description('OpenAI 兼容的生图 API 地址(末尾无需 /v1)。支持 OpenAI、SiliconFlow、豆包/火山方舟等。')
|
|
164
|
+
.default(''),
|
|
165
|
+
drawingApiKey: koishi_1.Schema.string()
|
|
166
|
+
.role('secret')
|
|
167
|
+
.description('生图 API Key。'),
|
|
168
|
+
drawingModel: koishi_1.Schema.string()
|
|
169
|
+
.description('生图模型名称,例如 gpt-image-1、dall-e-3、Kolors 等。')
|
|
170
|
+
.default('gpt-image-1'),
|
|
171
|
+
drawingSize: koishi_1.Schema.string()
|
|
172
|
+
.description('生图尺寸。')
|
|
173
|
+
.default('1024x1024')
|
|
174
|
+
}).description('生图 API 设置'),
|
|
119
175
|
koishi_1.Schema.object({
|
|
120
176
|
personaUserFilter: koishi_1.Schema.array(String)
|
|
121
177
|
.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
|
package/lib/plugins/command.js
CHANGED
|
@@ -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
|
package/lib/service/analysis.js
CHANGED
|
@@ -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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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);
|
|
@@ -714,6 +723,66 @@ class AnalysisService extends koishi_1.Service {
|
|
|
714
723
|
return null;
|
|
715
724
|
}
|
|
716
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
|
+
}
|
|
717
786
|
async analyzeGroupMessages(messages, selfId, target, context) {
|
|
718
787
|
this.ctx.logger.info(`开始分析 ${messages.length} 条消息...`);
|
|
719
788
|
const { userStats, totalChars, totalEmojiCount, allMessagesText } = (0, utils_1.calculateBasicStats)(messages);
|
|
@@ -737,6 +806,24 @@ class AnalysisService extends koishi_1.Service {
|
|
|
737
806
|
});
|
|
738
807
|
// Final statistics
|
|
739
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
|
+
}
|
|
740
827
|
const overallActiveHours = users.reduce((acc, user) => {
|
|
741
828
|
for (const hour in user.activeHours) {
|
|
742
829
|
acc[hour] = (acc[hour] || 0) + user.activeHours[hour];
|
|
@@ -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;
|
package/lib/service/llm.js
CHANGED
|
@@ -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
|
package/lib/service/message.js
CHANGED
|
@@ -52,12 +52,17 @@ class MessageService extends koishi_1.Service {
|
|
|
52
52
|
}
|
|
53
53
|
shouldListenToMessage(session) {
|
|
54
54
|
// TODO: private message
|
|
55
|
-
|
|
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
|
// 黑名单用户的消息完全不记录、不参与统计
|