mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +00:00
feat(话题分析): 用户头像引用展示
- TopicAnalyzer: Prompt中包含 [user_id],引导LLM使用ID引用用户 - ReportGenerator: 解析话题详情中的 [uid] 并替换为内联头像HTML - Templates: 更新所有主题的 topic_item.html 以支持富文本详情展示 - _conf_schema.json: 更新默认 Prompt,指导 LLM 使用 [用户ID] 引用用户 - ReportGenerator: 实现胶囊样式渲染 (头像+昵称),提升视觉体验
This commit is contained in:
@@ -11,3 +11,4 @@ data/cmd_config.json
|
||||
data/t2i_templates/astrbot_powershell.html
|
||||
data/t2i_templates/base.html
|
||||
.sisyphus/
|
||||
group-insight-main/
|
||||
+1
-1
@@ -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 代码块标记或其他格式。"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ class SummaryTopic:
|
||||
topic: str
|
||||
contributors: list[str]
|
||||
detail: str
|
||||
contributor_ids: list[str] = field(default_factory=list) # 贡献者ID列表 (用于显示头像)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'<span class="user-capsule" style="{capsule_style}">'
|
||||
f'<img src="{url}" style="{img_style}">'
|
||||
f'<span style="{name_style}">{name}</span>'
|
||||
f'</span>'
|
||||
)
|
||||
elif url:
|
||||
# 仅有头像回退
|
||||
return f'<img class="user-avatar-inline" src="{url}" style="width:1.3em;height:1.3em;border-radius:50%;vertical-align:text-bottom;margin:0 2px;">'
|
||||
|
||||
return match.group(0)
|
||||
|
||||
return re.sub(pattern, replacer, detail)
|
||||
|
||||
def _render_html_template(self, template: str, data: dict) -> str:
|
||||
"""HTML模板渲染,使用 {{key}} 占位符格式
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<span class="topic-title">{{ topic.topic.topic }}</span>
|
||||
</div>
|
||||
<div class="topic-contributors">参与者: {{ topic.contributors }}</div>
|
||||
<div class="topic-detail">{{ topic.topic.detail }}</div>
|
||||
<div class="topic-detail">{{ topic.detail }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="topic-content">
|
||||
<h3 class="topic-title">{{ topic.topic.topic }}</h3>
|
||||
<p class="topic-meta">PARTICIPANTS: {{ topic.contributors }}</p>
|
||||
<p class="topic-detail">{{ topic.topic.detail }}</p>
|
||||
<p class="topic-detail">{{ topic.detail }}</p>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
style="font-family: var(--font-hand); font-size: 1.05em; color: var(--ink-secondary); margin-bottom: 8px;">
|
||||
🙋♀️ 参与者: {{ topic.contributors }}
|
||||
</div>
|
||||
<div class="topic-detail">{{ topic.topic.detail }}</div>
|
||||
<div class="topic-detail">{{ topic.detail }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div style="margin-bottom: 10px; border-bottom: 1px dashed #ccc; padding-bottom: 5px;">
|
||||
<strong>#{{ item.index }} {{ item.topic.topic }}</strong>
|
||||
<span style="color: #666; font-size: 0.8em;">(参与者: {{ item.contributors }})</span>
|
||||
<p style="margin: 5px 0;">{{ item.topic.detail }}</p>
|
||||
<p style="margin: 5px 0;">{{ item.detail }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user