feat: update translate

This commit is contained in:
2026-07-24 10:17:58 +07:00
parent 9356ae6d8b
commit e492b84fc1
91 changed files with 3541 additions and 3438 deletions
+132 -136
View File
@@ -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