feat(trace): 实现语义化 Trace ID 机制并美化报告展现

- [TraceContext] 重构 ID 生成算法,支持包含群名和时间(如 manual_技术交流群_1733),极大提升日志调试效率。
 - [视觉美化] 从用户可见消息(初始提示及图片 Caption)中彻底移除技术性的 [ID: xxx] 字符串。
 - [去重增强] 在报告 Caption 中引入基于秒级时间戳的“隐式指纹”,配合共享正则匹配,确保精准去重。
 - [自动分析] 优化自动调度器逻辑,引入群名缓存,使服务端定时任务日志更具业务可读性。
 - [逻辑精简] 利用插件现有的任务并发锁,移除了 Trace ID 中冗余的 4 位随机后缀,进一步精简长度。
This commit is contained in:
SXP-Simon
2026-03-12 18:03:23 +08:00
parent 16c756259a
commit bd991e9c16
5 changed files with 113 additions and 31 deletions
@@ -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)
+6 -6
View File
@@ -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:
+36 -6
View File
@@ -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(
+47 -6
View File
@@ -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: