From bd991e9c16ee38014da1497bb7922efe904ef7fd Mon Sep 17 00:00:00 2001 From: SXP-Simon Date: Thu, 12 Mar 2026 18:03:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(trace):=20=E5=AE=9E=E7=8E=B0=E8=AF=AD?= =?UTF-8?q?=E4=B9=89=E5=8C=96=20Trace=20ID=20=E6=9C=BA=E5=88=B6=E5=B9=B6?= =?UTF-8?q?=E7=BE=8E=E5=8C=96=E6=8A=A5=E5=91=8A=E5=B1=95=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [TraceContext] 重构 ID 生成算法,支持包含群名和时间(如 manual_技术交流群_1733),极大提升日志调试效率。 - [视觉美化] 从用户可见消息(初始提示及图片 Caption)中彻底移除技术性的 [ID: xxx] 字符串。 - [去重增强] 在报告 Caption 中引入基于秒级时间戳的“隐式指纹”,配合共享正则匹配,确保精准去重。 - [自动分析] 优化自动调度器逻辑,引入群名缓存,使服务端定时任务日志更具业务可读性。 - [逻辑精简] 利用插件现有的任务并发锁,移除了 Trace ID 中冗余的 4 位随机后缀,进一步精简长度。 --- main.py | 26 ++++++--- .../platform/adapters/onebot_adapter.py | 11 ++-- src/infrastructure/reporting/dispatcher.py | 12 ++--- .../scheduler/auto_scheduler.py | 42 ++++++++++++--- src/shared/trace_context.py | 53 ++++++++++++++++--- 5 files changed, 113 insertions(+), 31 deletions(-) diff --git a/main.py b/main.py index 77f5ff1..da0212c 100644 --- a/main.py +++ b/main.py @@ -466,12 +466,23 @@ class GroupDailyAnalysis(Star): yield event.plain_result("❌ 此群未启用日常分析功能") return - # 设置 TraceID - trace_id = TraceContext.generate(prefix=f"manual_{group_id}") + # 获取群名以生成语义化的 TraceID + group_name = "" + try: + adapter = self.bot_manager.get_adapter(platform_id) + if adapter: + info = await adapter.get_group_info(group_id) + if info and info.group_name: + group_name = info.group_name + except Exception: + pass + + # 设置 TraceID (语义化格式: manual_群名_HHmm) + trace_id = TraceContext.generate(prefix="manual", group_name=group_name or group_id) TraceContext.set(trace_id) yield event.plain_result( - f"🔍 正在启动跨平台分析引擎,正在拉取最近消息...\n[ID: {trace_id}]" + "🔍 正在启动跨平台分析引擎,正在拉取最近消息..." ) try: @@ -492,7 +503,7 @@ class GroupDailyAnalysis(Star): f"📊 已获取{result['messages_count']}条消息,正在生成渲染报告..." ) - async for res in self._send_analysis_report(event, result, trace_id): + async for res in self._send_analysis_report(event, result): yield res except DuplicateGroupTaskError: @@ -504,7 +515,7 @@ class GroupDailyAnalysis(Star): ) async def _send_analysis_report( - self, event: AstrMessageEvent, result: dict, trace_id: str + self, event: AstrMessageEvent, result: dict ) -> AsyncGenerator: """处理分析结果的渲染和发送""" group_id = result["group_id"] @@ -536,17 +547,18 @@ class GroupDailyAnalysis(Star): ) if image_url: - caption = f"📊 每日群聊分析报告已生成:\n[ID: {trace_id}]" + caption = TraceContext.make_report_caption() await adapter.send_image(group_id, image_url, caption=caption) await self._try_upload_image(group_id, image_url, platform_id) elif html_content: yield event.plain_result("⚠️ 群分析报告图片发送失败,自动重试中。") + caption = TraceContext.make_report_caption() await self.retry_manager.add_task( html_content, analysis_result, group_id, platform_id, - caption=f"📊 每日群聊分析报告已生成:\n[ID: {trace_id}]", + caption=caption, ) else: text_report = self.report_generator.generate_text_report( diff --git a/src/infrastructure/platform/adapters/onebot_adapter.py b/src/infrastructure/platform/adapters/onebot_adapter.py index 88ebdc0..cb63a8a 100644 --- a/src/infrastructure/platform/adapters/onebot_adapter.py +++ b/src/infrastructure/platform/adapters/onebot_adapter.py @@ -22,6 +22,7 @@ from ....domain.value_objects.unified_message import ( MessageContentType, UnifiedMessage, ) +from ....shared.trace_context import REPORT_CAPTION_PATTERN from ....utils.logger import logger from ..base import PlatformAdapter @@ -644,14 +645,12 @@ class OneBotAdapter(PlatformAdapter): "[OneBot] was_image_sent_recently: 无法确定机器人 ID,历史回显校验可能不准确" ) - # [优化] 如果提供了 token,我们也尝试从 caption 中提取 ID 部分进行更精准匹配 + # [优化] 从 Caption 中提取基于时间戳的去重 Token search_token = None - if token and "[ID: " in token: - import re - - match = re.search(r"\[ID: ([^\]]+)\]", token) + if token: + match = REPORT_CAPTION_PATTERN.search(token) if match: - search_token = match.group(0) # 例如 "[ID: report_XXXX]" + search_token = match.group(0) # 例如 "| 03-12 17:33:20" for msg in reversed(messages): msg_time = msg.get("time", 0) diff --git a/src/infrastructure/reporting/dispatcher.py b/src/infrastructure/reporting/dispatcher.py index 51892e2..a96feb1 100644 --- a/src/infrastructure/reporting/dispatcher.py +++ b/src/infrastructure/reporting/dispatcher.py @@ -38,7 +38,7 @@ class ReportDispatcher: trace_id = TraceContext.get() output_format = self.config_manager.get_output_format() logger.info( - f"[{trace_id}] Dispatching report for group {group_id} (Format: {output_format})" + f"[{trace_id}] 正在分发群 {group_id} 的报告 (格式: {output_format})" ) success = False @@ -51,11 +51,11 @@ class ReportDispatcher: if success: logger.info( - f"[{trace_id}] Report dispatched successfully for group {group_id}" + f"[{trace_id}] 群 {group_id} 的报告分发成功" ) else: logger.warning( - f"[{trace_id}] Failed to dispatch report for group {group_id}" + f"[{trace_id}] 群 {group_id} 的报告分发失败" ) async def _dispatch_image( @@ -65,7 +65,7 @@ class ReportDispatcher: # 1. 检查渲染函数 if not self._html_render_func: logger.warning( - f"[{trace_id}] HTML render function not set, falling back to text." + f"[{trace_id}] 未设置 HTML 渲染函数,回退到文本模式。" ) return await self._dispatch_text(group_id, analysis_result, platform_id) @@ -94,7 +94,7 @@ class ReportDispatcher: # 3. 发送图片 if image_url: - caption = f"📊 每日群聊分析报告已生成:\n[ID: {trace_id}]" + caption = TraceContext.make_report_caption() sent = await self.message_sender.send_image_smart( group_id, image_url, caption, platform_id ) @@ -120,7 +120,7 @@ class ReportDispatcher: analysis_result, group_id, platform_id, - caption=f"📊 每日群聊分析报告已生成:\n[ID: {trace_id}]", + caption=TraceContext.make_report_caption(), ) return True # 已加入队列视作处理成功 (不在此处报错) else: diff --git a/src/infrastructure/scheduler/auto_scheduler.py b/src/infrastructure/scheduler/auto_scheduler.py index c65f733..860f283 100644 --- a/src/infrastructure/scheduler/auto_scheduler.py +++ b/src/infrastructure/scheduler/auto_scheduler.py @@ -49,6 +49,9 @@ class AutoScheduler: self.scheduler_job_ids = [] # 存储已注册的定时任务 ID self.last_executed_target = None # 记录上次执行的具体时间点,防止重复执行 + # Cache: group_id -> group_name (populated lazily) + self._group_name_cache: dict[str, str] = {} + def set_bot_instance(self, bot_instance): """设置bot实例(保持向后兼容)""" self.bot_manager.set_bot_instance(bot_instance) @@ -110,6 +113,30 @@ class AutoScheduler: except Exception as e: logger.error(f"❌ 获取平台ID失败: {e}") return None + async def _get_group_name_safe( + self, group_id: str, platform_id: str | None = None + ) -> str: + """ + 为 TraceID 生成解析可读的群名。 + 使用内存缓存以避免重复的 API 调用。 + 若名称不可用,则回退到 group_id。 + """ + if group_id in self._group_name_cache: + return self._group_name_cache[group_id] + + try: + pid = platform_id or await self.get_platform_id_for_group(group_id) + if pid: + adapter = self.bot_manager.get_adapter(pid) + if adapter: + info = await adapter.get_group_info(group_id) + if info and info.group_name: + self._group_name_cache[group_id] = info.group_name + return info.group_name + except Exception: + pass + + return group_id # ================================================================ # 任务注册与取消 @@ -381,8 +408,9 @@ class AutoScheduler: ): """为指定群执行自动分析(业务逻辑委派给 AnalysisApplicationService)""" try: - # 设置 TraceID - trace_id = TraceContext.generate(prefix=f"group_{group_id}") + # 解析可读群名以生成语义化的 TraceID + group_name = await self._get_group_name_safe(group_id, target_platform_id) + trace_id = TraceContext.generate(prefix="group", group_name=group_name) TraceContext.set(trace_id) logger.info( @@ -542,8 +570,9 @@ class AutoScheduler: ): """为指定群执行增量分析(业务逻辑委派给 AnalysisApplicationService)""" try: - # 设置 TraceID - trace_id = TraceContext.generate(prefix=f"incr_{group_id}") + # 解析可读群名以生成语义化的 TraceID + group_name = await self._get_group_name_safe(group_id, target_platform_id) + trace_id = TraceContext.generate(prefix="incr", group_name=group_name) TraceContext.set(trace_id) logger.info( @@ -683,8 +712,9 @@ class AutoScheduler: ): """为指定群生成增量最终报告(业务逻辑委派给 AnalysisApplicationService)""" try: - # 设置 TraceID - trace_id = TraceContext.generate(prefix=f"report_{group_id}") + # 解析可读群名以生成语义化的 TraceID + group_name = await self._get_group_name_safe(group_id, target_platform_id) + trace_id = TraceContext.generate(prefix="report", group_name=group_name) TraceContext.set(trace_id) logger.info( diff --git a/src/shared/trace_context.py b/src/shared/trace_context.py index 727ef90..f096b45 100644 --- a/src/shared/trace_context.py +++ b/src/shared/trace_context.py @@ -6,12 +6,20 @@ import functools import logging +import re import uuid from contextvars import ContextVar, Token from dataclasses import dataclass, field from datetime import datetime from typing import Any, Optional +# Trace ID 中群名的最大长度(平衡可读性和日志宽度) +_MAX_GROUP_NAME_LEN = 10 + +# 用于匹配报告 Caption 中去重 Token 的正则模式 +# 格式: "| MM-DD HH:MM:SS" +REPORT_CAPTION_PATTERN = re.compile(r"\| (\d{2}-\d{2} \d{2}:\d{2}:\d{2})") + # 当前追踪的上下文变量 _current_trace: ContextVar[Optional["TraceContext"]] = ContextVar( "current_trace", default=None @@ -146,18 +154,51 @@ class TraceContext: return new_ctx @staticmethod - def generate(prefix: str = "") -> str: + def generate(prefix: str = "", group_name: str = "") -> str: """ - [兼容性接口] 生成一个带前缀的唯一追踪 ID。 + 生成语义化、易读的追踪 ID。 + + 格式: {来源}_{群名}_{时间点} + 示例: manual_系统交流群_1733 + + 由于插件存在任务锁 (DuplicateGroupTaskError),确保了一个群同一时间只有一个分析任务, + 因此 时间点 (HHmm) 已足够提供唯一性,无需 UUID 缀。 Args: - prefix (str): 前缀,如 'manual_12345' + prefix (str): 来源标识,如 'manual', 'group', 'incr', 'report' + group_name (str): 可选群名,用于日志中快速识别 Returns: - str: 格式为 'prefix-uuid' 的字符串 + str: 语义化 TraceID 字符串 """ - uid = str(uuid.uuid4())[:8] - return f"{prefix}-{uid}" if prefix else uid + timestamp = datetime.now().strftime("%H%M") + + parts: list[str] = [] + if prefix: + parts.append(prefix) + if group_name: + # 清理:移除空白符和文件系统不安全字符 + safe_name = re.sub(r'[\s\n\r\t/\\:*?"<>|\[\]{}]', "", group_name) + safe_name = safe_name[:_MAX_GROUP_NAME_LEN] + if safe_name: + parts.append(safe_name) + parts.append(timestamp) + + return "_".join(parts) + + @staticmethod + def make_report_caption() -> str: + """ + 生成整洁的、面向用户的报告 Caption,包含用于去重的隐式时间戳。 + + 该时间戳用作图片去重检查的 Token。 + 格式: "📊 每日群聊分析报告已生成 | MM-DD HH:MM:SS" + + Returns: + str: 报告 Caption 字符串 + """ + ts = datetime.now().strftime("%m-%d %H:%M:%S") + return f"📊 每日群聊分析报告已生成 | {ts}" @classmethod def set(cls, trace_id: str) -> None: