mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 05:31:52 +00:00
feat: update translate
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
"""
|
||||
群日常分析插件
|
||||
基于群聊记录生成精美的日常分析报告,包含话题总结、用户画像、统计数据等
|
||||
Plugin phân tích hoạt động nhóm hằng ngày.
|
||||
|
||||
重构版本 - 使用模块化架构,支持跨平台
|
||||
Tạo báo cáo từ lịch sử trò chuyện, gồm tóm tắt chủ đề, hồ sơ thành viên và
|
||||
thống kê. Phiên bản module hoá hỗ trợ đa nền tảng.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -55,9 +55,9 @@ from .src.utils.resilience import GlobalRateLimiter
|
||||
|
||||
|
||||
class GroupDailyAnalysis(Star):
|
||||
"""群分析插件主类"""
|
||||
"""Lớp plugin phân tích nhóm chính."""
|
||||
|
||||
# ── 显式类型声明 (由 __init__ 初始化) ──
|
||||
# ── Khai báo kiểu tường minh, được khởi tạo trong __init__ ──
|
||||
config: AstrBotConfig
|
||||
config_manager: ConfigManager
|
||||
bot_manager: BotManager
|
||||
@@ -82,7 +82,7 @@ class GroupDailyAnalysis(Star):
|
||||
super().__init__(context)
|
||||
self.config = config
|
||||
|
||||
# 1. 基础设施层
|
||||
# 1. Tầng infrastructure.
|
||||
self.config_manager = ConfigManager(config)
|
||||
self.bot_manager = BotManager(self.config_manager)
|
||||
self.bot_manager.set_context(context)
|
||||
@@ -93,22 +93,22 @@ class GroupDailyAnalysis(Star):
|
||||
|
||||
self.report_generator = ReportGenerator(self.config_manager, plugin_data_dir)
|
||||
|
||||
# Telegram 注册表 (持久层)
|
||||
# Registry Telegram ở tầng persistence.
|
||||
self.platform_group_registry = PlatformGroupRegistry(self)
|
||||
|
||||
# 2. 领域层
|
||||
# 2. Tầng domain.
|
||||
activity_visualizer = ActivityVisualizer()
|
||||
self.statistics_service = StatisticsService(activity_visualizer)
|
||||
self.analysis_domain_service = AnalysisDomainService()
|
||||
|
||||
# 3. 分析核心 (LLM Bridge)
|
||||
# 3. Lõi phân tích, cầu nối LLM.
|
||||
self.llm_analyzer = LLMAnalyzer(context, self.config_manager)
|
||||
|
||||
# 4. 增量分析组件
|
||||
# 4. Thành phần phân tích gia tăng.
|
||||
self.incremental_store = IncrementalStore(self)
|
||||
self.incremental_merge_service = IncrementalMergeService()
|
||||
|
||||
# 5. 应用层
|
||||
# 5. Tầng application.
|
||||
self.analysis_service = AnalysisApplicationService(
|
||||
self.config_manager,
|
||||
self.bot_manager,
|
||||
@@ -121,7 +121,7 @@ class GroupDailyAnalysis(Star):
|
||||
incremental_merge_service=self.incremental_merge_service,
|
||||
)
|
||||
|
||||
# 消息处理服务
|
||||
# Dịch vụ xử lý tin nhắn.
|
||||
self.message_processing_service = MessageProcessingService(
|
||||
context, self.platform_group_registry
|
||||
)
|
||||
@@ -136,7 +136,7 @@ class GroupDailyAnalysis(Star):
|
||||
handlers=[self.telegram_template_preview_handler]
|
||||
)
|
||||
|
||||
# 调度与发送
|
||||
# Lập lịch và gửi.
|
||||
self.message_sender = MessageSender(self.bot_manager, self.config_manager)
|
||||
self.auto_scheduler = AutoScheduler(
|
||||
self.config_manager,
|
||||
@@ -147,15 +147,15 @@ class GroupDailyAnalysis(Star):
|
||||
plugin_instance=self,
|
||||
)
|
||||
|
||||
# 同步全局限流并进行初始化配置
|
||||
# Đồng bộ cấu hình bộ giới hạn toàn cục.
|
||||
GlobalRateLimiter.get_instance(self.config_manager.get_llm_max_concurrent())
|
||||
|
||||
self._initialized = False
|
||||
self._terminating = False # 生命周期标志
|
||||
self._terminating = False # Cờ vòng đời.
|
||||
self._init_lock = asyncio.Lock()
|
||||
self._background_tasks: set[asyncio.Task] = set()
|
||||
|
||||
# 异步注册任务,处理插件重载情况
|
||||
# Đăng ký tác vụ bất đồng bộ để xử lý reload plugin.
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
self._init_task = loop.create_task(
|
||||
@@ -166,18 +166,18 @@ class GroupDailyAnalysis(Star):
|
||||
except RuntimeError:
|
||||
self._init_task = None
|
||||
|
||||
# orchestrators 缓存已移至 应用层逻辑 (分析服务) 或 暂时移除以简化。
|
||||
# 如果需要高性能缓存,后续可由 AnalysisApplicationService 内部维护。
|
||||
# Cache orchestrator đã chuyển vào application service hoặc tạm bỏ để đơn giản.
|
||||
# Nếu cần cache hiệu năng cao, AnalysisApplicationService có thể quản lý nội bộ.
|
||||
|
||||
@filter.on_platform_loaded()
|
||||
async def on_platform_loaded(self):
|
||||
"""平台加载完成后初始化"""
|
||||
"""Khởi tạo sau khi nền tảng tải xong."""
|
||||
await self._run_initialization("Platform Loaded")
|
||||
|
||||
async def _run_initialization(self, source: str):
|
||||
"""统一初始化逻辑"""
|
||||
"""Logic khởi tạo thống nhất."""
|
||||
async with self._init_lock:
|
||||
# 如果已经成功发现过平台,且不是来自 Platform Loaded 的强制触发,则跳过
|
||||
# Bỏ qua nếu đã phát hiện nền tảng và không phải trigger Platform Loaded.
|
||||
if (
|
||||
self._initialized
|
||||
and self.bot_manager
|
||||
@@ -186,77 +186,78 @@ class GroupDailyAnalysis(Star):
|
||||
):
|
||||
return
|
||||
|
||||
# 稍微延迟,确保 context 和环境稳定
|
||||
# 针对极少数环境,2秒可能不足以让平台管理器就绪,增加到 5秒
|
||||
# Chờ để context, môi trường và platform manager ổn định.
|
||||
await asyncio.sleep(5)
|
||||
|
||||
# [加固] 如果在等待期间插件已被卸载(terminate),则直接退出
|
||||
# Thoát nếu plugin đã bị gỡ trong thời gian chờ.
|
||||
if not self.bot_manager:
|
||||
return
|
||||
|
||||
try:
|
||||
# 注册 TraceID 过滤器
|
||||
# Đăng ký bộ lọc TraceID.
|
||||
trace_filter = TraceLogFilter()
|
||||
if not any(
|
||||
isinstance(f, TraceLogFilter) for f in astrbot_logger.filters
|
||||
):
|
||||
astrbot_logger.addFilter(trace_filter)
|
||||
astrbot_logger.info("[Trace] TraceID 日志追踪已启用")
|
||||
astrbot_logger.info("[Trace] Đã bật theo dõi log bằng TraceID")
|
||||
|
||||
logger.info(f"正在执行插件初始化 (来源: {source})...")
|
||||
logger.info(f"Đang khởi tạo plugin (nguồn: {source})...")
|
||||
|
||||
# 0. 自动升级旧版 prompt 模板(str.format -> string.Template)并回写配置
|
||||
# 0. Tự nâng cấp prompt cũ từ str.format sang string.Template.
|
||||
try:
|
||||
self.config_manager.upgrade_prompt_templates()
|
||||
except Exception as e:
|
||||
logger.warning(f"自动升级 prompt 模板失败: {e}")
|
||||
logger.warning(f"Tự nâng cấp template prompt thất bại: {e}")
|
||||
|
||||
# 1. 尝试发现 bot 实例
|
||||
# 1. Thử phát hiện instance bot.
|
||||
await self.bot_manager.initialize_from_config()
|
||||
|
||||
# 2. 注册预览路由器
|
||||
# 2. Đăng ký router preview.
|
||||
if self.template_preview_router:
|
||||
await self.template_preview_router.ensure_handlers_registered(
|
||||
self.context
|
||||
)
|
||||
|
||||
# 3. 强制注册定时分析任务
|
||||
# 3. Đăng ký tác vụ phân tích định kỳ.
|
||||
if self.auto_scheduler:
|
||||
self.auto_scheduler.schedule_jobs(self.context)
|
||||
|
||||
self._initialized = True
|
||||
self._discovery_run = True
|
||||
logger.info(f"插件任务注册完成 (来源: {source})")
|
||||
logger.info(f"Hoàn tất đăng ký tác vụ plugin (nguồn: {source})")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"插件初始化失败: {e}", exc_info=True)
|
||||
logger.error(f"Khởi tạo plugin thất bại: {e}", exc_info=True)
|
||||
|
||||
async def terminate(self):
|
||||
"""插件被卸载/停用时调用,清理资源"""
|
||||
"""Dọn tài nguyên khi plugin bị gỡ hoặc vô hiệu hoá."""
|
||||
if self._terminating:
|
||||
return
|
||||
self._terminating = True
|
||||
|
||||
try:
|
||||
logger.info("开始清理群日常分析插件资源...")
|
||||
logger.info("Bắt đầu dọn tài nguyên plugin phân tích nhóm...")
|
||||
|
||||
# 1. 停止所有后台任务
|
||||
# 1. Dừng mọi tác vụ đang chạy.
|
||||
if self._background_tasks:
|
||||
logger.info(f"正在取消 {len(self._background_tasks)} 个运行中的任务...")
|
||||
logger.info(
|
||||
f"Đang huỷ {len(self._background_tasks)} tác vụ đang chạy..."
|
||||
)
|
||||
for task in self._background_tasks:
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
|
||||
# 等待任务结束,给予 3 秒宽限期
|
||||
# Chờ tác vụ kết thúc với thời gian gia hạn 3 giây.
|
||||
try:
|
||||
await asyncio.wait(list(self._background_tasks), timeout=3.0)
|
||||
except Exception:
|
||||
pass
|
||||
self._background_tasks.clear()
|
||||
|
||||
# 2. 停止各个组件 (顺序:先调度器,后底层服务)
|
||||
# 2. Dừng các thành phần: scheduler trước, dịch vụ tầng dưới sau.
|
||||
if self.auto_scheduler:
|
||||
logger.debug("正在停止自动调度器...")
|
||||
logger.debug("Đang dừng bộ lập lịch tự động...")
|
||||
self.auto_scheduler.unschedule_jobs(self.context)
|
||||
|
||||
if self.template_preview_router:
|
||||
@@ -265,32 +266,29 @@ class GroupDailyAnalysis(Star):
|
||||
if self.report_generator:
|
||||
await self.report_generator.close()
|
||||
|
||||
# 3. [关键修复] 只有在任务全部清理后,才清理引用。
|
||||
# 实际上,在 terminate 结束后,self 本身就会被 GC 释放,
|
||||
# 这里的显式 None 更多是为了协助循环引用清理,但由于异步任务存在竞态,
|
||||
# 我们可以通过 check _terminating 标志位来保护。
|
||||
# 为了彻底解决 #125,我们保留引用,让 GC 自然回收。
|
||||
logger.info("群日常分析插件资源清理完成")
|
||||
# 3. Chỉ dọn tham chiếu sau khi mọi tác vụ đã kết thúc.
|
||||
# Giữ tham chiếu để GC thu hồi tự nhiên, tránh race với tác vụ bất đồng bộ (#125).
|
||||
logger.info("Hoàn tất dọn tài nguyên plugin phân tích nhóm")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"插件资源清理失败: {e}")
|
||||
logger.error(f"Dọn tài nguyên plugin thất bại: {e}")
|
||||
|
||||
# ==================== Telegram 消息拦截器 ====================
|
||||
# ==================== Bộ chặn tin nhắn Telegram ====================
|
||||
|
||||
@filter.event_message_type(filter.EventMessageType.GROUP_MESSAGE)
|
||||
@filter.platform_adapter_type(filter.PlatformAdapterType.TELEGRAM)
|
||||
async def intercept_telegram_messages(self, event: AstrMessageEvent):
|
||||
"""
|
||||
拦截 Telegram 群消息并存储到数据库
|
||||
Chặn tin nhắn nhóm Telegram và lưu vào cơ sở dữ liệu.
|
||||
|
||||
委托给 MessageProcessingService 处理
|
||||
Uỷ quyền xử lý cho MessageProcessingService.
|
||||
"""
|
||||
try:
|
||||
await self.message_processing_service.process_message(event)
|
||||
except (ValueError, RuntimeError) as e:
|
||||
logger.warning(f"[Telegram] 消息存储失败: {e}")
|
||||
logger.warning(f"[Telegram] Lưu tin nhắn thất bại: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"[Telegram] 消息存储异常: {e}", exc_info=True)
|
||||
logger.error(f"[Telegram] Lỗi lưu tin nhắn: {e}", exc_info=True)
|
||||
|
||||
@filter.event_message_type(filter.EventMessageType.GROUP_MESSAGE)
|
||||
@filter.platform_adapter_type(
|
||||
@@ -298,7 +296,7 @@ class GroupDailyAnalysis(Star):
|
||||
| filter.PlatformAdapterType.QQOFFICIAL_WEBHOOK
|
||||
)
|
||||
async def intercept_qq_official_messages(self, event: AstrMessageEvent):
|
||||
"""缓存 QQ 官方机器人群消息;频道消息不在本插件适配范围内。"""
|
||||
"""Cache tin nhắn nhóm QQ Official; không xử lý tin nhắn kênh."""
|
||||
raw_message = getattr(getattr(event, "message_obj", None), "raw_message", None)
|
||||
if isinstance(raw_message, dict):
|
||||
author = raw_message.get("author") or {}
|
||||
@@ -316,23 +314,23 @@ class GroupDailyAnalysis(Star):
|
||||
try:
|
||||
await self.message_processing_service.process_message(event)
|
||||
except (ValueError, RuntimeError) as e:
|
||||
logger.warning(f"[QQOfficial] 消息存储失败: {e}")
|
||||
logger.warning(f"[QQOfficial] Lưu tin nhắn thất bại: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"[QQOfficial] 消息存储异常: {e}", exc_info=True)
|
||||
logger.error(f"[QQOfficial] Lỗi lưu tin nhắn: {e}", exc_info=True)
|
||||
|
||||
async def get_telegram_seen_group_ids(
|
||||
self, platform_id: str | None = None
|
||||
) -> list[str]:
|
||||
"""读取 Telegram 已见群/话题列表(给调度器回退使用)。"""
|
||||
"""Đọc nhóm/chủ đề Telegram đã thấy cho scheduler fallback."""
|
||||
return await self.platform_group_registry.get_all_group_ids(platform_id)
|
||||
|
||||
async def get_seen_group_ids(self, platform_id: str | None = None) -> list[str]:
|
||||
"""读取任意事件驱动平台已经见过的群组。"""
|
||||
"""Đọc các nhóm đã thấy trên mọi nền tảng hướng sự kiện."""
|
||||
return await self.platform_group_registry.get_all_group_ids(platform_id)
|
||||
|
||||
def _get_group_id_from_event(self, event: AstrMessageEvent) -> str | None:
|
||||
"""从消息事件中安全获取群组 ID"""
|
||||
# 保留此辅助方法,因为在其他 command 中仍被频繁使用
|
||||
"""Lấy an toàn ID nhóm từ sự kiện tin nhắn."""
|
||||
# Giữ helper này vì nhiều command khác vẫn dùng.
|
||||
try:
|
||||
group_id = event.get_group_id()
|
||||
return group_id if group_id else None
|
||||
@@ -340,12 +338,12 @@ class GroupDailyAnalysis(Star):
|
||||
return None
|
||||
|
||||
def _get_platform_id_from_event(self, event: AstrMessageEvent) -> str:
|
||||
"""从消息事件中获取平台唯一 ID"""
|
||||
# 保留此辅助方法,因为在其他 command 中仍被频繁使用
|
||||
"""Lấy ID nền tảng duy nhất từ sự kiện tin nhắn."""
|
||||
# Giữ helper này vì nhiều command khác vẫn dùng.
|
||||
try:
|
||||
return event.get_platform_id()
|
||||
except Exception:
|
||||
# 后备方案:从元数据获取
|
||||
# Fallback: lấy từ metadata.
|
||||
if (
|
||||
hasattr(event, "platform_meta")
|
||||
and event.platform_meta
|
||||
@@ -355,12 +353,12 @@ class GroupDailyAnalysis(Star):
|
||||
return "default"
|
||||
|
||||
# ================================================================
|
||||
# 图片报告上传到群文件 / 群相册(仅 QQ 平台 image 格式)
|
||||
# Upload báo cáo ảnh vào tệp/album nhóm, chỉ cho định dạng ảnh trên QQ.
|
||||
# ================================================================
|
||||
|
||||
async def _try_upload_image(self, group_id: str, image_url: str, platform_id: str):
|
||||
"""
|
||||
尝试将图片报告上传到群文件和/或群相册(静默处理,失败仅日志提示)。
|
||||
Thử upload báo cáo ảnh vào tệp và/hoặc album nhóm; lỗi chỉ ghi log.
|
||||
"""
|
||||
import base64
|
||||
import re
|
||||
@@ -376,12 +374,12 @@ class GroupDailyAnalysis(Star):
|
||||
if not adapter or not hasattr(adapter, "upload_group_file_to_folder"):
|
||||
return
|
||||
|
||||
# 1. 构造一个更友好的文件名
|
||||
# 1. Tạo tên tệp thân thiện hơn.
|
||||
now = datetime.now()
|
||||
timestamp = now.strftime("%H%M")
|
||||
date_str = now.strftime("%Y-%m-%d")
|
||||
|
||||
# 默认基础名和后缀
|
||||
# Tên cơ sở và phần mở rộng mặc định.
|
||||
ext = (
|
||||
".jpg"
|
||||
if (".jpg" in image_url.lower() or ".jpeg" in image_url.lower())
|
||||
@@ -390,41 +388,41 @@ class GroupDailyAnalysis(Star):
|
||||
nice_filename = f"bao_cao_phan_tich_nhom_{group_id}_{date_str}_{timestamp}{ext}"
|
||||
|
||||
try:
|
||||
# 尝试通过适配器获取群名称,使文件名更具辨识度
|
||||
# Thử lấy tên nhóm qua adapter để tên tệp dễ nhận diện hơn.
|
||||
group_info = await adapter.get_group_info(group_id)
|
||||
if group_info and group_info.group_name:
|
||||
# 过滤非法文件名字符:\ / : * ? " < > |
|
||||
# Lọc ký tự không hợp lệ trong tên tệp: \ / : * ? " < > |
|
||||
safe_name = re.sub(r'[\\/:*?"<>|]', "", group_info.group_name).strip()
|
||||
if safe_name:
|
||||
nice_filename = f"bao_cao_phan_tich_nhom_{safe_name}_{date_str}_{timestamp}{ext}"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. 将内容准备为文件或数据
|
||||
# 2. Chuẩn bị nội dung dưới dạng tệp hoặc dữ liệu.
|
||||
image_file = None
|
||||
created_temp = False
|
||||
MAX_PAYLOAD_SIZE = 20 * 1024 * 1024 # 20MB 限制
|
||||
MAX_PAYLOAD_SIZE = 20 * 1024 * 1024 # Giới hạn 20 MB.
|
||||
|
||||
try:
|
||||
data = None
|
||||
if image_url.startswith("base64://"):
|
||||
base64_str = image_url[len("base64://") :]
|
||||
if len(base64_str) * 3 / 4 > MAX_PAYLOAD_SIZE:
|
||||
logger.warning("图片上传失败:Base64 负载过大")
|
||||
logger.warning("Upload ảnh thất bại: payload Base64 quá lớn")
|
||||
return
|
||||
data = base64.b64decode(base64_str)
|
||||
elif image_url.startswith("data:"):
|
||||
parts = image_url.split(",", 1)
|
||||
if len(parts) == 2:
|
||||
if len(parts[1]) * 3 / 4 > MAX_PAYLOAD_SIZE:
|
||||
logger.warning("图片上传失败:Data URI 负载过大")
|
||||
logger.warning("Upload ảnh thất bại: payload Data URI quá lớn")
|
||||
return
|
||||
data = base64.b64decode(parts[1])
|
||||
elif os.path.isfile(image_url):
|
||||
image_file = os.path.abspath(image_url)
|
||||
|
||||
if data and not image_file:
|
||||
# 使用 tempfile 生成唯一后缀,防止并发冲突
|
||||
# Dùng tempfile tạo hậu tố duy nhất để tránh xung đột đồng thời.
|
||||
fd, image_file = tempfile.mkstemp(suffix=ext, prefix="group_report_")
|
||||
try:
|
||||
with os.fdopen(fd, "wb") as f:
|
||||
@@ -437,7 +435,7 @@ class GroupDailyAnalysis(Star):
|
||||
if not image_file:
|
||||
return
|
||||
|
||||
# 3. 执行上传:群文件
|
||||
# 3. Upload vào tệp nhóm.
|
||||
if enable_file:
|
||||
try:
|
||||
folder_name = self.config_manager.get_group_file_folder()
|
||||
@@ -450,10 +448,10 @@ class GroupDailyAnalysis(Star):
|
||||
group_id=group_id,
|
||||
file_path=image_file,
|
||||
folder_id=folder_id,
|
||||
filename=nice_filename, # 显式传递漂亮的文件名
|
||||
filename=nice_filename, # Truyền tường minh tên tệp thân thiện.
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"群文件上传失败 (群 {group_id}): {e}")
|
||||
logger.warning(f"Upload tệp nhóm thất bại (nhóm {group_id}): {e}")
|
||||
|
||||
if enable_album and hasattr(adapter, "upload_group_album"):
|
||||
try:
|
||||
@@ -465,12 +463,12 @@ class GroupDailyAnalysis(Star):
|
||||
album_id = await adapter.find_album_id(group_id, album_name) # type: ignore[attr-defined]
|
||||
if not album_id and strict_mode:
|
||||
logger.info(
|
||||
f"群相册严格模式开启:在群 {group_id} 中未找到名为 '{album_name}' 的相册,停止上传。"
|
||||
f"Đã bật chế độ album nghiêm ngặt: không tìm thấy album '{album_name}' trong nhóm {group_id}, dừng upload"
|
||||
)
|
||||
return
|
||||
elif strict_mode:
|
||||
logger.info(
|
||||
f"群相册严格模式开启:未设置目标相册名称,停止上传以防止操作群 {group_id} 的默认相册。"
|
||||
f"Đã bật chế độ album nghiêm ngặt nhưng chưa đặt tên album đích; dừng để tránh thao tác album mặc định của nhóm {group_id}"
|
||||
)
|
||||
return
|
||||
await adapter.upload_group_album( # type: ignore[attr-defined]
|
||||
@@ -481,9 +479,9 @@ class GroupDailyAnalysis(Star):
|
||||
strict_mode=strict_mode,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"群相册上传失败 (群 {group_id}): {e}")
|
||||
logger.warning(f"Upload album nhóm thất bại (nhóm {group_id}): {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"图片上传处理异常: {e}")
|
||||
logger.warning(f"Lỗi xử lý upload ảnh: {e}")
|
||||
finally:
|
||||
if created_temp and image_file and os.path.exists(image_file):
|
||||
try:
|
||||
@@ -497,8 +495,8 @@ class GroupDailyAnalysis(Star):
|
||||
self, event: AstrMessageEvent, days: int | None = None
|
||||
):
|
||||
"""
|
||||
分析群聊日常活动(跨平台支持)
|
||||
用法: /群分析 [天数]
|
||||
Phân tích hoạt động nhóm hằng ngày trên nhiều nền tảng.
|
||||
Cách dùng: /phantichnhom [số ngày]
|
||||
"""
|
||||
if self._terminating:
|
||||
return
|
||||
@@ -508,7 +506,7 @@ class GroupDailyAnalysis(Star):
|
||||
self._background_tasks.add(current_task)
|
||||
|
||||
try:
|
||||
event.should_call_llm(True) # 阻止默认 LLM 解析
|
||||
event.should_call_llm(True) # Ngăn LLM mặc định phân tích.
|
||||
group_id = self._get_group_id_from_event(event)
|
||||
platform_id = self._get_platform_id_from_event(event)
|
||||
|
||||
@@ -516,10 +514,10 @@ class GroupDailyAnalysis(Star):
|
||||
yield event.plain_result("❌ Vui lòng sử dụng lệnh này trong nhóm chat")
|
||||
return
|
||||
|
||||
# 更新bot实例
|
||||
# Cập nhật instance bot.
|
||||
self.bot_manager.update_from_event(event)
|
||||
|
||||
# 优先使用 UMO 进行权限检查 (兼容白名单 UMO 格式)
|
||||
# Ưu tiên UMO để kiểm tra quyền và tương thích whitelist UMO.
|
||||
check_target = getattr(event, "unified_msg_origin", None)
|
||||
if not check_target:
|
||||
check_target = f"{platform_id}:GroupMessage:{group_id}"
|
||||
@@ -534,7 +532,7 @@ class GroupDailyAnalysis(Star):
|
||||
)
|
||||
return
|
||||
|
||||
# 获取群名以生成语义化的 TraceID
|
||||
# Lấy tên nhóm để tạo TraceID có ngữ nghĩa.
|
||||
group_name = ""
|
||||
try:
|
||||
adapter = self.bot_manager.get_adapter(platform_id)
|
||||
@@ -545,20 +543,20 @@ class GroupDailyAnalysis(Star):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 设置 TraceID (语义化格式: manual_群名_HHmm)
|
||||
# Thiết lập TraceID theo dạng manual_tên_nhóm_HHmm.
|
||||
trace_id = TraceContext.generate(
|
||||
prefix="manual", group_name=group_name or group_id
|
||||
)
|
||||
TraceContext.set(trace_id)
|
||||
|
||||
# 表情回应 或 文本提示(二选一,由配置开关控制)
|
||||
# Reaction hoặc thông báo văn bản, chọn theo cấu hình.
|
||||
adapter = self.bot_manager.get_adapter(platform_id)
|
||||
orig_msg_id = getattr(event.message_obj, "message_id", None)
|
||||
adapter_platform_name = (
|
||||
(adapter.get_platform_name() if adapter else "").strip().lower()
|
||||
)
|
||||
# QQ 官方机器人 API v2 不支持本插件使用的表情回应接口,
|
||||
# 因此始终沿用原有的文字进度提示,避免触发无效的 reaction 请求。
|
||||
# API v2 của QQ Official không hỗ trợ reaction plugin đang dùng,
|
||||
# nên luôn dùng thông báo tiến độ dạng văn bản.
|
||||
use_text_reply = (
|
||||
adapter_platform_name in {"qq_official", "qq_official_webhook"}
|
||||
or self.config_manager.get_enable_analysis_reply()
|
||||
@@ -573,7 +571,7 @@ class GroupDailyAnalysis(Star):
|
||||
event.get_group_id(), orig_msg_id, "analysis_started"
|
||||
)
|
||||
|
||||
# 调用 DDD 应用级服务
|
||||
# Gọi application service theo DDD.
|
||||
result = await self.analysis_service.execute_daily_analysis(
|
||||
group_id=group_id, platform_id=platform_id, manual=True, days=days
|
||||
)
|
||||
@@ -586,7 +584,7 @@ class GroupDailyAnalysis(Star):
|
||||
)
|
||||
elif reason == "muted":
|
||||
logger.warning(
|
||||
f"群 {group_id} 开启了全群禁言或对 Bot 禁言,跳过回复以防抛出发送异常"
|
||||
f"Nhóm {group_id} đã tắt chat toàn nhóm hoặc tắt quyền bot; bỏ qua phản hồi để tránh lỗi gửi"
|
||||
)
|
||||
else:
|
||||
yield event.plain_result(
|
||||
@@ -607,9 +605,9 @@ class GroupDailyAnalysis(Star):
|
||||
"📊 Phân tích cho nhóm này đang chạy, vui lòng thử lại sau nhé~"
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
logger.info("群分析任务被取消 (插件重载或卸载)")
|
||||
logger.info("Tác vụ phân tích nhóm đã bị huỷ do plugin reload hoặc bị gỡ")
|
||||
except Exception as e:
|
||||
logger.error(f"群分析失败: {e}", exc_info=True)
|
||||
logger.error(f"Phân tích nhóm thất bại: {e}", exc_info=True)
|
||||
yield event.plain_result(
|
||||
f"❌ Phân tích thất bại: {str(e)}. Vui lòng kiểm tra kết nối "
|
||||
"mạng, cấu hình LLM hoặc liên hệ quản trị viên"
|
||||
@@ -621,9 +619,9 @@ class GroupDailyAnalysis(Star):
|
||||
async def _send_analysis_report(
|
||||
self, event: AstrMessageEvent, result: dict
|
||||
) -> AsyncGenerator:
|
||||
"""处理分析结果的渲染和发送"""
|
||||
"""Render và gửi kết quả phân tích."""
|
||||
if self._terminating or not self.config_manager:
|
||||
logger.warning("插件正在关闭,停止发送报告")
|
||||
logger.warning("Plugin đang đóng, dừng gửi báo cáo")
|
||||
return
|
||||
|
||||
group_id = result["group_id"]
|
||||
@@ -633,7 +631,7 @@ class GroupDailyAnalysis(Star):
|
||||
output_format = self.config_manager.get_output_format()[0]
|
||||
is_qq_official = adapter.get_platform_name() == "qq_official"
|
||||
|
||||
# 定义获取回调
|
||||
# Định nghĩa callback truy xuất dữ liệu.
|
||||
async def avatar_url_getter(user_id: str) -> str | None:
|
||||
return await adapter.get_user_avatar_url(user_id)
|
||||
|
||||
@@ -666,10 +664,12 @@ class GroupDailyAnalysis(Star):
|
||||
sent = await adapter.send_image(group_id, image_url, caption=caption)
|
||||
if sent:
|
||||
await self._try_upload_image(group_id, image_url, platform_id)
|
||||
return # 成功发送
|
||||
return # Gửi thành công.
|
||||
|
||||
# 如果图片生成或发送失败,直接回退到文本
|
||||
logger.warning(f"图片报告发送失败,正在发送文本回退报告。群: {group_id}")
|
||||
# Chuyển thẳng sang văn bản nếu tạo hoặc gửi ảnh thất bại.
|
||||
logger.warning(
|
||||
f"Gửi báo cáo ảnh thất bại, đang gửi fallback văn bản cho nhóm {group_id}"
|
||||
)
|
||||
await self._send_text_reports(
|
||||
group_id, analysis_result, is_qq_official, adapter
|
||||
)
|
||||
@@ -690,10 +690,10 @@ class GroupDailyAnalysis(Star):
|
||||
|
||||
if is_only_url:
|
||||
if base_url and base_url.strip():
|
||||
# 获取配置中的输出目录
|
||||
# Lấy thư mục output trong cấu hình.
|
||||
html_output_dir = self.config_manager.get_html_output_dir()
|
||||
|
||||
# 若用户配置为空,使用默认目录
|
||||
# Dùng thư mục mặc định nếu cấu hình rỗng.
|
||||
if not html_output_dir:
|
||||
from astrbot.api.star import StarTools
|
||||
|
||||
@@ -702,7 +702,7 @@ class GroupDailyAnalysis(Star):
|
||||
"self_hosted_html_reports",
|
||||
)
|
||||
|
||||
# 计算相对路径并转换为URL
|
||||
# Tính đường dẫn tương đối và chuyển thành URL.
|
||||
rel_path = os.path.relpath(html_path, html_output_dir)
|
||||
url_path = rel_path.replace(os.sep, "/")
|
||||
report_url = f"{base_url.rstrip('/')}/{url_path.lstrip('/')}"
|
||||
@@ -710,15 +710,15 @@ class GroupDailyAnalysis(Star):
|
||||
yield event.plain_result(
|
||||
f"📊 Báo cáo phân tích nhóm hôm nay đã sẵn sàng:\n{report_url}"
|
||||
)
|
||||
return # 拦截成功,直接退出,不再发文件
|
||||
return # Đã gửi liên kết, không gửi tệp nữa.
|
||||
else:
|
||||
logger.warning(
|
||||
f"手动触发群 {group_id} 开启了仅发送外链,但未配置 html_base_url,回退至发送文件。"
|
||||
f"Nhóm {group_id} được kích hoạt thủ công và chỉ bật gửi liên kết nhưng chưa cấu hình html_base_url; chuyển sang gửi tệp"
|
||||
)
|
||||
|
||||
caption = self.report_generator.build_html_caption(html_path)
|
||||
|
||||
# 发送 HTML 文件
|
||||
# Gửi tệp HTML.
|
||||
sender = getattr(self, "message_sender", None)
|
||||
if sender:
|
||||
sent = await sender.send_file(
|
||||
@@ -776,10 +776,10 @@ class GroupDailyAnalysis(Star):
|
||||
@filter.permission_type(PermissionType.ADMIN)
|
||||
async def set_output_format(self, event: AstrMessageEvent, format_input: str = ""):
|
||||
"""
|
||||
设置分析报告输出格式(跨平台支持)
|
||||
用法: /设置格式 [格式名称或序号] 或 image,html 等逗号分隔的组合
|
||||
Thiết lập định dạng báo cáo trên nhiều nền tảng.
|
||||
Cách dùng: /dinhdang [tên hoặc số thứ tự], có thể dùng ``image,html``.
|
||||
"""
|
||||
# 命令由插件处理,禁用默认 LLM 回退。
|
||||
# Plugin xử lý command, tắt fallback LLM mặc định.
|
||||
event.should_call_llm(True)
|
||||
|
||||
available_formats = ["image", "text", "html"]
|
||||
@@ -806,19 +806,19 @@ Cách dùng: /dinhdang [tên hoặc số thứ tự], ví dụ: /dinhdang image,
|
||||
return
|
||||
|
||||
target_format = None
|
||||
# 尝试由序号选择
|
||||
# Thử chọn theo số thứ tự.
|
||||
if format_input.isdigit():
|
||||
idx = int(format_input) - 1
|
||||
if 0 <= idx < len(available_formats):
|
||||
target_format = available_formats[idx]
|
||||
|
||||
# 尝试按名称选择
|
||||
# Thử chọn theo tên.
|
||||
if not target_format:
|
||||
input_lower = format_input.lower()
|
||||
if input_lower in available_formats:
|
||||
target_format = input_lower
|
||||
|
||||
# 支持逗号分隔的多个格式
|
||||
# Hỗ trợ nhiều định dạng phân tách bằng dấu phẩy.
|
||||
if not target_format:
|
||||
parts = [f.strip() for f in format_input.replace(",", ",").split(",")]
|
||||
if all(p in available_formats for p in parts) and len(parts) > 1:
|
||||
@@ -853,10 +853,10 @@ Cách dùng: /dinhdang [tên hoặc số thứ tự], ví dụ: /dinhdang image,
|
||||
self, event: AstrMessageEvent, template_input: str = ""
|
||||
):
|
||||
"""
|
||||
设置分析报告模板(跨平台支持)
|
||||
用法: /设置模板 [模板名称或序号]
|
||||
Thiết lập mẫu báo cáo trên nhiều nền tảng.
|
||||
Cách dùng: /maubc [tên mẫu hoặc số thứ tự].
|
||||
"""
|
||||
# 命令由插件处理,禁用默认 LLM 回退。
|
||||
# Plugin xử lý command, tắt fallback LLM mặc định.
|
||||
event.should_call_llm(True)
|
||||
|
||||
available_templates = (
|
||||
@@ -901,10 +901,10 @@ Cách dùng: /maubc [tên mẫu hoặc số thứ tự]
|
||||
@filter.permission_type(PermissionType.ADMIN)
|
||||
async def view_templates(self, event: AstrMessageEvent):
|
||||
"""
|
||||
查看所有可用的报告模板及预览图(跨平台支持)
|
||||
用法: /查看模板
|
||||
Xem mọi mẫu báo cáo khả dụng và ảnh preview trên nhiều nền tảng.
|
||||
Cách dùng: /xemmau.
|
||||
"""
|
||||
# 命令由插件处理,禁用默认 LLM 回退。
|
||||
# Plugin xử lý command, tắt fallback LLM mặc định.
|
||||
event.should_call_llm(True)
|
||||
|
||||
available_templates = (
|
||||
@@ -943,15 +943,11 @@ Cách dùng: /maubc [tên mẫu hoặc số thứ tự]
|
||||
@filter.permission_type(PermissionType.ADMIN)
|
||||
async def analysis_settings(self, event: AstrMessageEvent, action: str = "status"):
|
||||
"""
|
||||
管理分析设置(跨平台支持)
|
||||
用法: /分析设置 [enable|disable|status|reload|test]
|
||||
- enable: 启用当前群的分析功能
|
||||
- disable: 禁用当前群的分析功能
|
||||
- status: 查看当前状态
|
||||
- reload: 重新加载配置并重启定时任务
|
||||
- test: 测试自动分析功能
|
||||
- filter_bot: 切换是否在分析中包含机器人自己的消息
|
||||
- incremental_debug: 切换增量分析立即报告模式(调试用)
|
||||
Quản lý cài đặt phân tích trên nhiều nền tảng.
|
||||
|
||||
Cách dùng: /caidat [enable|disable|status|reload|test|filter_bot|incremental_debug].
|
||||
``filter_bot`` chuyển chế độ lọc tin bot; ``incremental_debug`` chuyển
|
||||
chế độ gửi ngay báo cáo gia tăng để debug.
|
||||
"""
|
||||
group_id = self._get_group_id_from_event(event)
|
||||
|
||||
@@ -987,7 +983,7 @@ Cách dùng: /maubc [tên mẫu hoặc số thứ tự]
|
||||
|
||||
yield event.plain_result("🧪 Đang kiểm tra tính năng phân tích tự động...")
|
||||
|
||||
# 更新bot实例(用于测试)
|
||||
# Cập nhật instance bot để kiểm tra.
|
||||
self.bot_manager.update_from_event(event)
|
||||
|
||||
try:
|
||||
@@ -1041,7 +1037,7 @@ Cách dùng: /maubc [tên mẫu hoặc số thứ tự]
|
||||
output_format = self.config_manager.get_output_format()[0]
|
||||
min_threshold = self.config_manager.get_min_messages_threshold()
|
||||
|
||||
# 增量分析状态
|
||||
# Trạng thái phân tích gia tăng.
|
||||
incremental_enabled = self.config_manager.get_incremental_enabled()
|
||||
incremental_status_text = "Chưa bật"
|
||||
if incremental_enabled:
|
||||
@@ -1075,7 +1071,7 @@ Cách dùng: /maubc [tên mẫu hoặc số thứ tự]
|
||||
@filter.command("tangcuong", alias={"incremental_status", "增量状态"})
|
||||
@filter.permission_type(PermissionType.ADMIN)
|
||||
async def incremental_status(self, event: AstrMessageEvent):
|
||||
"""查看当前增量分析状态(滑动窗口)"""
|
||||
"""Xem trạng thái phân tích gia tăng trong cửa sổ trượt."""
|
||||
group_id = self._get_group_id_from_event(event)
|
||||
if not group_id:
|
||||
yield event.plain_result("❌ Vui lòng sử dụng lệnh này trong nhóm chat")
|
||||
@@ -1089,12 +1085,12 @@ Cách dùng: /maubc [tên mẫu hoặc số thứ tự]
|
||||
|
||||
import time as time_mod
|
||||
|
||||
# 计算滑动窗口范围
|
||||
# Tính phạm vi cửa sổ trượt.
|
||||
analysis_days = self.config_manager.get_analysis_days()
|
||||
window_end = time_mod.time()
|
||||
window_start = window_end - (analysis_days * 24 * 3600)
|
||||
|
||||
# 查询窗口内的批次
|
||||
# Truy vấn các batch trong cửa sổ.
|
||||
batches = await self.incremental_store.query_batches(
|
||||
group_id, window_start, window_end
|
||||
)
|
||||
@@ -1110,7 +1106,7 @@ Cách dùng: /maubc [tên mẫu hoặc số thứ tự]
|
||||
)
|
||||
return
|
||||
|
||||
# 合并批次获取聚合视图
|
||||
# Gộp batch để tạo chế độ xem tổng hợp.
|
||||
state = self.incremental_merge_service.merge_batches(
|
||||
batches, window_start, window_end
|
||||
)
|
||||
@@ -1127,7 +1123,7 @@ Cách dùng: /maubc [tên mẫu hoặc số thứ tự]
|
||||
)
|
||||
|
||||
async def _handle_settings_enable(self, event: AstrMessageEvent, group_id: str):
|
||||
"""协助逻辑:处理启用设置的分支逻辑"""
|
||||
"""Helper xử lý nhánh bật cài đặt."""
|
||||
mode = self.config_manager.get_group_list_mode()
|
||||
target_id = event.unified_msg_origin or group_id
|
||||
|
||||
@@ -1164,7 +1160,7 @@ Cách dùng: /maubc [tên mẫu hoặc số thứ tự]
|
||||
)
|
||||
|
||||
async def _handle_settings_disable(self, event: AstrMessageEvent, group_id: str):
|
||||
"""协助逻辑:处理禁用设置的分支逻辑"""
|
||||
"""Helper xử lý nhánh tắt cài đặt."""
|
||||
mode = self.config_manager.get_group_list_mode()
|
||||
target_id = event.unified_msg_origin or group_id
|
||||
|
||||
|
||||
Reference in New Issue
Block a user