mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +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("❌ 此群未启用日常分析功能")
|
yield event.plain_result("❌ 此群未启用日常分析功能")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 设置 TraceID
|
# 获取群名以生成语义化的 TraceID
|
||||||
trace_id = TraceContext.generate(prefix=f"manual_{group_id}")
|
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)
|
TraceContext.set(trace_id)
|
||||||
|
|
||||||
yield event.plain_result(
|
yield event.plain_result(
|
||||||
f"🔍 正在启动跨平台分析引擎,正在拉取最近消息...\n[ID: {trace_id}]"
|
"🔍 正在启动跨平台分析引擎,正在拉取最近消息..."
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -492,7 +503,7 @@ class GroupDailyAnalysis(Star):
|
|||||||
f"📊 已获取{result['messages_count']}条消息,正在生成渲染报告..."
|
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
|
yield res
|
||||||
|
|
||||||
except DuplicateGroupTaskError:
|
except DuplicateGroupTaskError:
|
||||||
@@ -504,7 +515,7 @@ class GroupDailyAnalysis(Star):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def _send_analysis_report(
|
async def _send_analysis_report(
|
||||||
self, event: AstrMessageEvent, result: dict, trace_id: str
|
self, event: AstrMessageEvent, result: dict
|
||||||
) -> AsyncGenerator:
|
) -> AsyncGenerator:
|
||||||
"""处理分析结果的渲染和发送"""
|
"""处理分析结果的渲染和发送"""
|
||||||
group_id = result["group_id"]
|
group_id = result["group_id"]
|
||||||
@@ -536,17 +547,18 @@ class GroupDailyAnalysis(Star):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if image_url:
|
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 adapter.send_image(group_id, image_url, caption=caption)
|
||||||
await self._try_upload_image(group_id, image_url, platform_id)
|
await self._try_upload_image(group_id, image_url, platform_id)
|
||||||
elif html_content:
|
elif html_content:
|
||||||
yield event.plain_result("⚠️ 群分析报告图片发送失败,自动重试中。")
|
yield event.plain_result("⚠️ 群分析报告图片发送失败,自动重试中。")
|
||||||
|
caption = TraceContext.make_report_caption()
|
||||||
await self.retry_manager.add_task(
|
await self.retry_manager.add_task(
|
||||||
html_content,
|
html_content,
|
||||||
analysis_result,
|
analysis_result,
|
||||||
group_id,
|
group_id,
|
||||||
platform_id,
|
platform_id,
|
||||||
caption=f"📊 每日群聊分析报告已生成:\n[ID: {trace_id}]",
|
caption=caption,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
text_report = self.report_generator.generate_text_report(
|
text_report = self.report_generator.generate_text_report(
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from ....domain.value_objects.unified_message import (
|
|||||||
MessageContentType,
|
MessageContentType,
|
||||||
UnifiedMessage,
|
UnifiedMessage,
|
||||||
)
|
)
|
||||||
|
from ....shared.trace_context import REPORT_CAPTION_PATTERN
|
||||||
from ....utils.logger import logger
|
from ....utils.logger import logger
|
||||||
from ..base import PlatformAdapter
|
from ..base import PlatformAdapter
|
||||||
|
|
||||||
@@ -644,14 +645,12 @@ class OneBotAdapter(PlatformAdapter):
|
|||||||
"[OneBot] was_image_sent_recently: 无法确定机器人 ID,历史回显校验可能不准确"
|
"[OneBot] was_image_sent_recently: 无法确定机器人 ID,历史回显校验可能不准确"
|
||||||
)
|
)
|
||||||
|
|
||||||
# [优化] 如果提供了 token,我们也尝试从 caption 中提取 ID 部分进行更精准匹配
|
# [优化] 从 Caption 中提取基于时间戳的去重 Token
|
||||||
search_token = None
|
search_token = None
|
||||||
if token and "[ID: " in token:
|
if token:
|
||||||
import re
|
match = REPORT_CAPTION_PATTERN.search(token)
|
||||||
|
|
||||||
match = re.search(r"\[ID: ([^\]]+)\]", token)
|
|
||||||
if match:
|
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):
|
for msg in reversed(messages):
|
||||||
msg_time = msg.get("time", 0)
|
msg_time = msg.get("time", 0)
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class ReportDispatcher:
|
|||||||
trace_id = TraceContext.get()
|
trace_id = TraceContext.get()
|
||||||
output_format = self.config_manager.get_output_format()
|
output_format = self.config_manager.get_output_format()
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[{trace_id}] Dispatching report for group {group_id} (Format: {output_format})"
|
f"[{trace_id}] 正在分发群 {group_id} 的报告 (格式: {output_format})"
|
||||||
)
|
)
|
||||||
|
|
||||||
success = False
|
success = False
|
||||||
@@ -51,11 +51,11 @@ class ReportDispatcher:
|
|||||||
|
|
||||||
if success:
|
if success:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[{trace_id}] Report dispatched successfully for group {group_id}"
|
f"[{trace_id}] 群 {group_id} 的报告分发成功"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"[{trace_id}] Failed to dispatch report for group {group_id}"
|
f"[{trace_id}] 群 {group_id} 的报告分发失败"
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _dispatch_image(
|
async def _dispatch_image(
|
||||||
@@ -65,7 +65,7 @@ class ReportDispatcher:
|
|||||||
# 1. 检查渲染函数
|
# 1. 检查渲染函数
|
||||||
if not self._html_render_func:
|
if not self._html_render_func:
|
||||||
logger.warning(
|
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)
|
return await self._dispatch_text(group_id, analysis_result, platform_id)
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ class ReportDispatcher:
|
|||||||
|
|
||||||
# 3. 发送图片
|
# 3. 发送图片
|
||||||
if image_url:
|
if image_url:
|
||||||
caption = f"📊 每日群聊分析报告已生成:\n[ID: {trace_id}]"
|
caption = TraceContext.make_report_caption()
|
||||||
sent = await self.message_sender.send_image_smart(
|
sent = await self.message_sender.send_image_smart(
|
||||||
group_id, image_url, caption, platform_id
|
group_id, image_url, caption, platform_id
|
||||||
)
|
)
|
||||||
@@ -120,7 +120,7 @@ class ReportDispatcher:
|
|||||||
analysis_result,
|
analysis_result,
|
||||||
group_id,
|
group_id,
|
||||||
platform_id,
|
platform_id,
|
||||||
caption=f"📊 每日群聊分析报告已生成:\n[ID: {trace_id}]",
|
caption=TraceContext.make_report_caption(),
|
||||||
)
|
)
|
||||||
return True # 已加入队列视作处理成功 (不在此处报错)
|
return True # 已加入队列视作处理成功 (不在此处报错)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ class AutoScheduler:
|
|||||||
self.scheduler_job_ids = [] # 存储已注册的定时任务 ID
|
self.scheduler_job_ids = [] # 存储已注册的定时任务 ID
|
||||||
self.last_executed_target = None # 记录上次执行的具体时间点,防止重复执行
|
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):
|
def set_bot_instance(self, bot_instance):
|
||||||
"""设置bot实例(保持向后兼容)"""
|
"""设置bot实例(保持向后兼容)"""
|
||||||
self.bot_manager.set_bot_instance(bot_instance)
|
self.bot_manager.set_bot_instance(bot_instance)
|
||||||
@@ -110,6 +113,30 @@ class AutoScheduler:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ 获取平台ID失败: {e}")
|
logger.error(f"❌ 获取平台ID失败: {e}")
|
||||||
return None
|
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)"""
|
"""为指定群执行自动分析(业务逻辑委派给 AnalysisApplicationService)"""
|
||||||
try:
|
try:
|
||||||
# 设置 TraceID
|
# 解析可读群名以生成语义化的 TraceID
|
||||||
trace_id = TraceContext.generate(prefix=f"group_{group_id}")
|
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)
|
TraceContext.set(trace_id)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -542,8 +570,9 @@ class AutoScheduler:
|
|||||||
):
|
):
|
||||||
"""为指定群执行增量分析(业务逻辑委派给 AnalysisApplicationService)"""
|
"""为指定群执行增量分析(业务逻辑委派给 AnalysisApplicationService)"""
|
||||||
try:
|
try:
|
||||||
# 设置 TraceID
|
# 解析可读群名以生成语义化的 TraceID
|
||||||
trace_id = TraceContext.generate(prefix=f"incr_{group_id}")
|
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)
|
TraceContext.set(trace_id)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -683,8 +712,9 @@ class AutoScheduler:
|
|||||||
):
|
):
|
||||||
"""为指定群生成增量最终报告(业务逻辑委派给 AnalysisApplicationService)"""
|
"""为指定群生成增量最终报告(业务逻辑委派给 AnalysisApplicationService)"""
|
||||||
try:
|
try:
|
||||||
# 设置 TraceID
|
# 解析可读群名以生成语义化的 TraceID
|
||||||
trace_id = TraceContext.generate(prefix=f"report_{group_id}")
|
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)
|
TraceContext.set(trace_id)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -6,12 +6,20 @@
|
|||||||
|
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from contextvars import ContextVar, Token
|
from contextvars import ContextVar, Token
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Optional
|
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: ContextVar[Optional["TraceContext"]] = ContextVar(
|
||||||
"current_trace", default=None
|
"current_trace", default=None
|
||||||
@@ -146,18 +154,51 @@ class TraceContext:
|
|||||||
return new_ctx
|
return new_ctx
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate(prefix: str = "") -> str:
|
def generate(prefix: str = "", group_name: str = "") -> str:
|
||||||
"""
|
"""
|
||||||
[兼容性接口] 生成一个带前缀的唯一追踪 ID。
|
生成语义化、易读的追踪 ID。
|
||||||
|
|
||||||
|
格式: {来源}_{群名}_{时间点}
|
||||||
|
示例: manual_系统交流群_1733
|
||||||
|
|
||||||
|
由于插件存在任务锁 (DuplicateGroupTaskError),确保了一个群同一时间只有一个分析任务,
|
||||||
|
因此 时间点 (HHmm) 已足够提供唯一性,无需 UUID 缀。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
prefix (str): 前缀,如 'manual_12345'
|
prefix (str): 来源标识,如 'manual', 'group', 'incr', 'report'
|
||||||
|
group_name (str): 可选群名,用于日志中快速识别
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: 格式为 'prefix-uuid' 的字符串
|
str: 语义化 TraceID 字符串
|
||||||
"""
|
"""
|
||||||
uid = str(uuid.uuid4())[:8]
|
timestamp = datetime.now().strftime("%H%M")
|
||||||
return f"{prefix}-{uid}" if prefix else uid
|
|
||||||
|
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
|
@classmethod
|
||||||
def set(cls, trace_id: str) -> None:
|
def set(cls, trace_id: str) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user