koishi-plugin-group-analysis-mx 1.2.5 → 1.2.7
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 +42 -12
- package/lib/service/renderer.js +12 -3
- 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
|
@@ -814,6 +814,18 @@ class AnalysisService extends koishi_1.Service {
|
|
|
814
814
|
this.ctx.logger.info(`开始分析 ${messages.length} 条消息...`);
|
|
815
815
|
const { userStats, totalChars, totalEmojiCount, allMessagesText } = (0, utils_1.calculateBasicStats)(messages);
|
|
816
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');
|
|
817
829
|
// LLM analyses in parallel
|
|
818
830
|
const users = Object.values(userStats);
|
|
819
831
|
const [topics, userTitles, goldenQuotes] = await Promise.all([
|
|
@@ -821,7 +833,7 @@ class AnalysisService extends koishi_1.Service {
|
|
|
821
833
|
this.config.userTitleAnalysis
|
|
822
834
|
? this.ctx.group_analysis_llm.analyzeUserTitles(users, context)
|
|
823
835
|
: Promise.resolve([]),
|
|
824
|
-
this.ctx.group_analysis_llm.analyzeGoldenQuotes(messagesText, this.config.maxGoldenQuotes, context)
|
|
836
|
+
this.ctx.group_analysis_llm.analyzeGoldenQuotes(idBasedMessagesText || messagesText, this.config.maxGoldenQuotes, context)
|
|
825
837
|
]).catch((error) => {
|
|
826
838
|
this.ctx.logger.error('LLM analysis failed:', error);
|
|
827
839
|
// On LLM failure, return empty results to avoid crashing the entire analysis.
|
|
@@ -894,21 +906,39 @@ class AnalysisService extends koishi_1.Service {
|
|
|
894
906
|
}
|
|
895
907
|
});
|
|
896
908
|
}
|
|
897
|
-
//
|
|
909
|
+
// 金句:LLM 输入用 [user_id],这里按 ID 精确回填完整昵称(含 emoji),并补全头像。
|
|
898
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);
|
|
899
919
|
goldenQuotes.forEach((quote) => {
|
|
900
920
|
if (quote && quote.sender) {
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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) : '') ||
|
|
911
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
|
+
}
|
|
912
942
|
}
|
|
913
943
|
}
|
|
914
944
|
});
|
package/lib/service/renderer.js
CHANGED
|
@@ -54,6 +54,15 @@ class RendererService extends koishi_1.Service {
|
|
|
54
54
|
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
/** 往渲染 HTML 注入 emoji 字体栈,避免 puppeteer 截图时 emoji 显示为方框/问号。 */
|
|
58
|
+
injectEmojiFont(html) {
|
|
59
|
+
if (!html || typeof html !== 'string')
|
|
60
|
+
return html;
|
|
61
|
+
const style = `<style>html,body{font-family:-apple-system,'Segoe UI','Segoe UI Emoji','Noto Sans SC','Noto Color Emoji','Apple Color Emoji','Microsoft YaHei','Hiragino Sans GB',sans-serif !important;}</style>`;
|
|
62
|
+
if (html.includes('</head>'))
|
|
63
|
+
return html.replace('</head>', style + '</head>');
|
|
64
|
+
return html;
|
|
65
|
+
}
|
|
57
66
|
async init() {
|
|
58
67
|
const resourcesDir = __dirname + '/../../resources';
|
|
59
68
|
const templateDir = this.templateDir;
|
|
@@ -184,7 +193,7 @@ class RendererService extends koishi_1.Service {
|
|
|
184
193
|
dynamicAvatarUrl: dynamicAvatarBase64
|
|
185
194
|
});
|
|
186
195
|
// 写入临时 HTML 文件
|
|
187
|
-
await fs_1.promises.writeFile(outTemplateHtmlPath, filledHtml);
|
|
196
|
+
await fs_1.promises.writeFile(outTemplateHtmlPath, this.injectEmojiFont(filledHtml));
|
|
188
197
|
this.ctx.logger.info('HTML 模板填充完成,正在调用 Puppeteer 进行渲染...');
|
|
189
198
|
const page = await this.ctx.puppeteer.page();
|
|
190
199
|
// 重新加载页面并使用 goto 访问本地文件
|
|
@@ -255,7 +264,7 @@ class RendererService extends koishi_1.Service {
|
|
|
255
264
|
const html = env.render('html_template.html', context);
|
|
256
265
|
const randomId = Math.random().toString(36).substring(2, 15);
|
|
257
266
|
const outTemplateHtmlPath = path_1.default.resolve(skinDir, `${randomId}.html`);
|
|
258
|
-
await fs_1.promises.writeFile(outTemplateHtmlPath, html);
|
|
267
|
+
await fs_1.promises.writeFile(outTemplateHtmlPath, this.injectEmojiFont(html));
|
|
259
268
|
this.ctx.logger.info(`原项目皮肤 ${skin} HTML 生成完成,正在调用 Puppeteer 渲染...`);
|
|
260
269
|
const page = await this.ctx.puppeteer.page();
|
|
261
270
|
await page.setViewport({ width: 1080, height: 1920, deviceScaleFactor: 2 });
|
|
@@ -471,7 +480,7 @@ class RendererService extends koishi_1.Service {
|
|
|
471
480
|
theme,
|
|
472
481
|
dynamicAvatarUrl: dynamicAvatarBase64
|
|
473
482
|
});
|
|
474
|
-
await fs_1.promises.writeFile(outTemplateHtmlPath, filledHtml);
|
|
483
|
+
await fs_1.promises.writeFile(outTemplateHtmlPath, this.injectEmojiFont(filledHtml));
|
|
475
484
|
this.ctx.logger.info('用户画像 HTML 模板填充完成,正在调用 Puppeteer 进行渲染...');
|
|
476
485
|
const page = await this.ctx.puppeteer.page();
|
|
477
486
|
// 重新加载页面并使用 goto 访问本地文件
|
package/package.json
CHANGED