mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 05:31:52 +00:00
feat(trace): 实现语义化 Trace ID 机制并美化报告展现
- [TraceContext] 重构 ID 生成算法,支持包含群名和时间(如 manual_技术交流群_1733),极大提升日志调试效率。 - [视觉美化] 从用户可见消息(初始提示及图片 Caption)中彻底移除技术性的 [ID: xxx] 字符串。 - [去重增强] 在报告 Caption 中引入基于秒级时间戳的“隐式指纹”,配合共享正则匹配,确保精准去重。 - [自动分析] 优化自动调度器逻辑,引入群名缓存,使服务端定时任务日志更具业务可读性。 - [逻辑精简] 利用插件现有的任务并发锁,移除了 Trace ID 中冗余的 4 位随机后缀,进一步精简长度。
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user