diff --git a/main.py b/main.py index e580626..d3a2946 100644 --- a/main.py +++ b/main.py @@ -326,6 +326,16 @@ class QQGroupDailyAnalysis(Star): async def avatar_getter(user_id: str) -> str | None: return await adapter.get_user_avatar_url(user_id) + # 定义昵称获取回调 + async def nickname_getter(user_id: str) -> str | None: + try: + member = await adapter.get_member_info(group_id, user_id) + if member: + return member.card or member.nickname + except Exception: + pass + return None + if output_format == "image": ( image_url, @@ -335,6 +345,7 @@ class QQGroupDailyAnalysis(Star): group_id, self.html_render, avatar_getter=avatar_getter, + nickname_getter=nickname_getter, ) if image_url: @@ -355,7 +366,10 @@ class QQGroupDailyAnalysis(Star): elif output_format == "pdf": pdf_path = await self.report_generator.generate_pdf_report( - analysis_result, group_id, avatar_getter=avatar_getter + analysis_result, + group_id, + avatar_getter=avatar_getter, + nickname_getter=nickname_getter, ) if pdf_path: if not await adapter.send_file(group_id, pdf_path): diff --git a/src/infrastructure/reporting/generators.py b/src/infrastructure/reporting/generators.py index 0c5530e..5d96e26 100644 --- a/src/infrastructure/reporting/generators.py +++ b/src/infrastructure/reporting/generators.py @@ -30,6 +30,7 @@ class ReportGenerator(IReportGenerator): group_id: str, html_render_func, avatar_getter=None, + nickname_getter=None, ) -> tuple[str | None, str | None]: """ 生成图片格式的分析报告 @@ -50,6 +51,7 @@ class ReportGenerator(IReportGenerator): analysis_result, chart_template="activity_chart.html", avatar_getter=avatar_getter, + nickname_getter=nickname_getter, ) # 先渲染HTML模板(使用异步方法) @@ -238,6 +240,7 @@ class ReportGenerator(IReportGenerator): analysis_result: dict, chart_template: str = "activity_chart.html", avatar_getter=None, + nickname_getter=None, ) -> dict: """准备渲染数据""" stats = analysis_result["statistics"] @@ -253,7 +256,7 @@ class ReportGenerator(IReportGenerator): for i, topic in enumerate(topics[:max_topics], 1): # 处理话题详情中的用户引用头像 processed_detail = await self._process_topic_detail( - topic.detail, avatar_getter, user_analysis + topic.detail, avatar_getter, nickname_getter, user_analysis ) topics_list.append( { @@ -349,7 +352,11 @@ class ReportGenerator(IReportGenerator): return render_data async def _process_topic_detail( - self, detail: str, avatar_getter, user_analysis: dict = None + self, + detail: str, + avatar_getter, + nickname_getter=None, + user_analysis: dict = None, ) -> str: """ 处理话题详情,将 [123456] 格式的用户引用替换为头像+名称的胶囊样式 @@ -361,46 +368,60 @@ class ReportGenerator(IReportGenerator): 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): + async def replacer(match): uid = match.group(1) - url = avatars.get(uid) + url = await self._get_user_avatar( + uid, avatar_getter + ) # 内部已有缓存,无需顶层并发获取 name = None + # 1. 尝试从 LLM 分析结果获取 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;" - ) + # 2. 尝试通过回调获取实时昵称 + if not name and nickname_getter: + try: + name = await nickname_getter(uid) + except Exception as e: + logger.warning(f"获取昵称失败 {uid}: {e}") - return ( - f'' - f'' - f'{name}' - f"" - ) - elif url: - # 仅有头像回退 - return f'' + # 胶囊样式 (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 match.group(0) + # 确保有头像 URL(Base64 或 默认) + if not url: + url = self._get_default_avatar_base64() - return re.sub(pattern, replacer, detail) + return ( + f'' + f'' + f'{name}' + f"" + ) + + # re.sub 不支持异步回调,需要先提取所有 ID 进行处理,或者使用自定义的替换逻辑 + # 这里为了保持异步特性,我们需要手动处理 + + # 1. 找出所有匹配项 + matches = list(re.finditer(pattern, detail)) + if not matches: + return detail + + # 2. 从后往前替换,保持索引正确 + result = detail + for match in reversed(matches): + replacement = await replacer(match) + start, end = match.span() + result = result[:start] + replacement + result[end:] + + return result def _render_html_template(self, template: str, data: dict) -> str: """HTML模板渲染,使用 {{key}} 占位符格式