diff --git a/.gitignore b/.gitignore index dc51f89..4798225 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ data/cmd_config.json data/t2i_templates/astrbot_powershell.html data/t2i_templates/base.html .sisyphus/ +group-insight-main/ \ No newline at end of file diff --git a/_conf_schema.json b/_conf_schema.json index 8fa8d3c..6aa4a98 100644 --- a/_conf_schema.json +++ b/_conf_schema.json @@ -316,7 +316,7 @@ "type": "text", "editor_mode": true, "editor_language": "markdown", - "default": "你是一个帮我进行群聊信息总结的助手,生成总结内容时,你需要严格遵守下面的几个准则:\n\n请分析接下来提供的群聊记录,提取出最多 **{max_topics}** 个主要话题。\n\n## 对于每个话题,请提供:\n\n1. **话题名称**(突出主题内容,尽量简明扼要)\n2. **主要参与者**(最多5人)\n3. **话题详细描述**(包含关键信息和结论)\n\n## 注意事项:\n\n- 对于比较有价值的点,稍微用一两句话详细讲讲,比如不要生成 \"Nolan 和 SOV 讨论了 galgame 中关于性符号的衍生情况\" 这种宽泛的内容,而是生成更加具体的讨论内容,让其他人只看这个消息就能知道讨论中有价值的、有营养的信息。\n- 对于其中的部分信息,你需要特意提到主题施加的主体是谁,是哪个群友做了什么事情,而不要直接生成和群友没有关系的语句。\n- 对于每一条总结,尽量讲清楚前因后果,以及话题的结论:是什么、为什么、怎么做。如果用户没有讲到细节,则可以不用这么做。\n\n## 群聊记录:\n\n{messages_text}\n\n---\n\n## 重要:必须返回标准 JSON 格式\n\n严格遵守以下规则:\n\n1. 只使用英文双引号 `\"` ,不要使用中文引号 `\"` `\"`\n2. 字符串内容中的引号必须转义为 `\\\"`\n3. 多个对象之间用逗号分隔\n4. 数组元素之间用逗号分隔\n5. 不要在 JSON 外添加任何文字说明\n6. 描述内容避免使用特殊符号,用普通文字表达\n\n### 返回格式示例:\n\n```json\n[\n {{\n \"topic\": \"话题名称\",\n \"contributors\": [\"用户1\", \"用户2\"],\n \"detail\": \"话题描述内容\"\n }},\n {{\n \"topic\": \"另一个话题\",\n \"contributors\": [\"用户3\", \"用户4\"],\n \"detail\": \"另一个话题的描述\"\n }}\n]\n```\n\n**注意**:返回的内容必须是纯 JSON,不要包含 markdown 代码块标记或其他格式。" + "default": "你是一个帮我进行群聊信息总结的助手,生成总结内容时,你需要严格遵守下面的几个准则:\n\n请分析接下来提供的群聊记录,提取出最多 **{max_topics}** 个主要话题。\n\n## 对于每个话题,请提供:\n\n1. **话题名称**(突出主题内容,尽量简明扼要)\n2. **主要参与者**(最多5人)\n3. **话题详细描述**(包含关键信息和结论)\n\n## 注意事项:\n\n- 对于比较有价值的点,稍微用一两句话详细讲讲,比如不要生成 \"Nolan 和 SOV 讨论了 galgame 中关于性符号的衍生情况\" 这种宽泛的内容,而是生成更加具体的讨论内容,让其他人只看这个消息就能知道讨论中有价值的、有营养的信息。\n- 对于其中的部分信息,你需要特意提到主题施加的主体是谁,是哪个群友做了什么事情,而不要直接生成和群友没有关系的语句。\n- 对于每一条总结,尽量讲清楚前因后果,以及话题的结论:是什么、为什么、怎么做。如果用户没有讲到细节,则可以不用这么做。\\n- **用户引用**:在话题详情描述中,如果提到了具体用户,请使用 `[用户ID]` 的格式来指代(例如 `[123456]`)。不要只写昵称。我们会自动渲染头像。\\n\\n## 群聊记录:\n\n{messages_text}\n\n---\n\n## 重要:必须返回标准 JSON 格式\n\n严格遵守以下规则:\n\n1. 只使用英文双引号 `\"` ,不要使用中文引号 `\"` `\"`\n2. 字符串内容中的引号必须转义为 `\\\"`\n3. 多个对象之间用逗号分隔\n4. 数组元素之间用逗号分隔\n5. 不要在 JSON 外添加任何文字说明\n6. 描述内容避免使用特殊符号,用普通文字表达\n\n### 返回格式示例:\n\n```json\n[\n {{\n \"topic\": \"话题名称\",\n \"contributors\": [\"用户1\", \"用户2\"],\n \"detail\": \"话题描述内容\"\n }},\n {{\n \"topic\": \"另一个话题\",\n \"contributors\": [\"用户3\", \"用户4\"],\n \"detail\": \"另一个话题的描述\"\n }}\n]\n```\n\n**注意**:返回的内容必须是纯 JSON,不要包含 markdown 代码块标记或其他格式。" } } }, diff --git a/src/application/services/analysis_application_service.py b/src/application/services/analysis_application_service.py index 46752e2..c077caf 100644 --- a/src/application/services/analysis_application_service.py +++ b/src/application/services/analysis_application_service.py @@ -309,7 +309,12 @@ class AnalysisApplicationService: # 8. 构建 IncrementalBatch # 8a. 转换话题: SummaryTopic -> dict new_topics = [ - {"topic": t.topic, "contributors": t.contributors, "detail": t.detail} + { + "topic": t.topic, + "contributors": t.contributors, + "detail": t.detail, + "contributor_ids": t.contributor_ids, + } for t in topics ] diff --git a/src/domain/models/data_models.py b/src/domain/models/data_models.py index 4492767..b9ddef1 100644 --- a/src/domain/models/data_models.py +++ b/src/domain/models/data_models.py @@ -13,6 +13,7 @@ class SummaryTopic: topic: str contributors: list[str] detail: str + contributor_ids: list[str] = field(default_factory=list) # 贡献者ID列表 (用于显示头像) @dataclass diff --git a/src/domain/services/incremental_merge_service.py b/src/domain/services/incremental_merge_service.py index 92ddee0..a954c40 100644 --- a/src/domain/services/incremental_merge_service.py +++ b/src/domain/services/incremental_merge_service.py @@ -237,6 +237,7 @@ class IncrementalMergeService: topic=topic_dict.get("topic", "未知话题"), contributors=topic_dict.get("contributors", []), detail=topic_dict.get("detail", ""), + contributor_ids=topic_dict.get("contributor_ids", []), ) topics.append(topic) diff --git a/src/infrastructure/analysis/analyzers/topic_analyzer.py b/src/infrastructure/analysis/analyzers/topic_analyzer.py index 2e16883..ead87b1 100644 --- a/src/infrastructure/analysis/analyzers/topic_analyzer.py +++ b/src/infrastructure/analysis/analyzers/topic_analyzer.py @@ -130,7 +130,12 @@ class TopicAnalyzer(BaseAnalyzer): cleaned_text = re.sub(r"[\x00-\x1f\x7f-\x9f]", "", cleaned_text) text_messages.append( - {"sender": nickname, "time": msg_time, "content": cleaned_text} + { + "sender": nickname, + "time": msg_time, + "content": cleaned_text, + "user_id": str(user_id), + } ) except Exception as e: logger.error( @@ -145,7 +150,7 @@ class TopicAnalyzer(BaseAnalyzer): # 构建消息文本 messages_text = "\n".join( [ - f"[{msg['time']}] {msg['sender']}: {msg['content']}" + f"[{msg['time']}] [{msg['user_id']}] {msg['sender']}: {msg['content']}" for msg in text_messages ] ) @@ -289,6 +294,7 @@ class TopicAnalyzer(BaseAnalyzer): "sender": nickname, "time": msg_time, "content": cleaned_text.strip(), + "user_id": str(sender.get("user_id", "")), } ) return text_messages @@ -332,8 +338,34 @@ class TopicAnalyzer(BaseAnalyzer): logger.debug(f"第一条文本消息类型: {type(text_messages[0])}") logger.debug(f"第一条文本消息内容: {text_messages[0]}") + # 建立昵称到ID的映射表 + nickname_to_id = {} + for msg in text_messages: + sender = msg.get("sender") + user_id = msg.get("user_id") + if sender and user_id: + nickname_to_id[sender] = user_id + # 直接传入原始消息,让 build_prompt 方法处理 - return await self.analyze(messages, umo, session_id) + topics, usage = await self.analyze(messages, umo, session_id) + + # 回填贡献者 ID + for topic in topics: + ids = set() + for contributor in topic.contributors: + # 尝试精确匹配 + if contributor in nickname_to_id: + ids.add(nickname_to_id[contributor]) + else: + # 尝试模糊匹配(LLM可能会简化名字) + for nick, uid in nickname_to_id.items(): + if contributor in nick or nick in contributor: + ids.add(uid) + # 找到一个匹配即可,避免过度匹配 + break + topic.contributor_ids = list(ids) + + return topics, usage except Exception as e: logger.error(f"话题分析失败: {e}", exc_info=True) diff --git a/src/infrastructure/reporting/generators.py b/src/infrastructure/reporting/generators.py index 70f185d..c78da0a 100644 --- a/src/infrastructure/reporting/generators.py +++ b/src/infrastructure/reporting/generators.py @@ -237,12 +237,19 @@ class ReportGenerator(IReportGenerator): # 使用Jinja2模板构建话题HTML(批量渲染) max_topics = self.config_manager.get_max_topics() topics_list = [] + user_analysis = analysis_result.get("user_analysis") + for i, topic in enumerate(topics[:max_topics], 1): + # 处理话题详情中的用户引用头像 + processed_detail = await self._process_topic_detail( + topic.detail, avatar_getter, user_analysis + ) topics_list.append( { "index": i, "topic": topic, "contributors": "、".join(topic.contributors), + "detail": processed_detail, } ) @@ -330,6 +337,58 @@ class ReportGenerator(IReportGenerator): logger.info(f"渲染数据准备完成,包含 {len(render_data)} 个字段") return render_data + async def _process_topic_detail( + self, detail: str, avatar_getter, user_analysis: dict = None + ) -> str: + """ + 处理话题详情,将 [123456] 格式的用户引用替换为头像+名称的胶囊样式 + """ + import re + + pattern = r"\[(\d+)\]" + matches = re.findall(pattern, detail) + if not matches: + return detail + + unique_ids = set(matches) + avatars = {} + + # 并发获取头像 + for uid in unique_ids: + avatars[uid] = await self._get_user_avatar(uid, avatar_getter) + + def replacer(match): + uid = match.group(1) + url = avatars.get(uid) + + name = None + if user_analysis and uid in user_analysis: + name = user_analysis[uid].get("name") + + if url and name: + # 胶囊样式 (Capsule Style) + capsule_style = ( + "display:inline-flex;align-items:center;background:rgba(0,0,0,0.05);" + "padding:2px 6px 2px 2px;border-radius:12px;margin:0 2px;" + "vertical-align:middle;border:1px solid rgba(0,0,0,0.1);text-decoration:none;" + ) + img_style = "width:18px;height:18px;border-radius:50%;margin-right:4px;display:block;" + name_style = "font-size:0.85em;color:inherit;font-weight:500;line-height:1;" + + return ( + f'' + f'' + f'{name}' + f'' + ) + elif url: + # 仅有头像回退 + return f'' + + return match.group(0) + + return re.sub(pattern, replacer, detail) + def _render_html_template(self, template: str, data: dict) -> str: """HTML模板渲染,使用 {{key}} 占位符格式 diff --git a/src/infrastructure/reporting/templates/format/topic_item.html b/src/infrastructure/reporting/templates/format/topic_item.html index 70f96de..7ee761e 100644 --- a/src/infrastructure/reporting/templates/format/topic_item.html +++ b/src/infrastructure/reporting/templates/format/topic_item.html @@ -9,7 +9,7 @@ {{ topic.topic.topic }}
参与者: {{ topic.contributors }}
-
{{ topic.topic.detail }}
+
{{ topic.detail }}
{% endfor %} diff --git a/src/infrastructure/reporting/templates/retro_futurism/topic_item.html b/src/infrastructure/reporting/templates/retro_futurism/topic_item.html index 78ba10a..234f13a 100644 --- a/src/infrastructure/reporting/templates/retro_futurism/topic_item.html +++ b/src/infrastructure/reporting/templates/retro_futurism/topic_item.html @@ -10,7 +10,7 @@

{{ topic.topic.topic }}

PARTICIPANTS: {{ topic.contributors }}

-

{{ topic.topic.detail }}

+

{{ topic.detail }}

{% endfor %} diff --git a/src/infrastructure/reporting/templates/scrapbook/topic_item.html b/src/infrastructure/reporting/templates/scrapbook/topic_item.html index 8dc606e..4231ec8 100644 --- a/src/infrastructure/reporting/templates/scrapbook/topic_item.html +++ b/src/infrastructure/reporting/templates/scrapbook/topic_item.html @@ -26,7 +26,7 @@ style="font-family: var(--font-hand); font-size: 1.05em; color: var(--ink-secondary); margin-bottom: 8px;"> 🙋‍♀️ 参与者: {{ topic.contributors }} -
{{ topic.topic.detail }}
+
{{ topic.detail }}
{% endfor %} diff --git a/src/infrastructure/reporting/templates/simple/topic_item.html b/src/infrastructure/reporting/templates/simple/topic_item.html index 8a74aeb..047d640 100644 --- a/src/infrastructure/reporting/templates/simple/topic_item.html +++ b/src/infrastructure/reporting/templates/simple/topic_item.html @@ -5,7 +5,7 @@
#{{ item.index }} {{ item.topic.topic }} (参与者: {{ item.contributors }}) -

{{ item.topic.detail }}

+

{{ item.detail }}

{% endfor %}