mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +00:00
feat: 全面重构主插件架构,解决并发竞态、资源泄露并优化代码健壮性
【并发与生命周期规范化】 - 引入 asyncio.Lock (_init_lock) 保护初始化流程,确保多入口(Reload/Init/Loaded)触发时维持单例运行,消除重复注册风险。 - 重构初始化任务创建逻辑,增加对当前协程 Loop 的状态检查,预防 Python 3.10+ 环境下常见的运行时异常。 - 引入 _background_tasks 集合统一追踪异步任务生命周期,实现透明的任务注册与自动移除。 【资源清理与加固】 - 增强 terminate() 析构逻辑:在卸载时主动取消所有追踪的后台任务(包括正在进行的分析、渲染或依赖安装),并使用 asyncio.gather 确保安全释放。 - 采用 type: ignore 配合非 Optional 类型声明,在保证 terminate() 能够置空引用的前提下,消除了 Pylance 对全局 50+ 处属性访问的“可能为 None”误报。 【业务流程健壮性】 - 提取超长逻辑:将分析报告的渲染与多格式分发逻辑抽离至 _send_analysis_report 异步生成器,显著降低单一函数圈复杂度。 - 优化图片上传服务: * 引入 MAX_PAYLOAD_SIZE (20MB) 机制,拦截超大 Base64/数据,隔离内存风险。 * 弃用固定路径,改用 tempfile.mkstemp 生成线程安全的唯一临时文件。 * 完善 finally 块清理逻辑,确保物理文件在上传成功或失败后都能被即时删除。 【指令系统重构】 - 采用子命令模式优化 /分析设置 (analysis_settings),将启用/禁用逻辑下沉至私有处理方法,提升代码可读性。 - 修复 /群分析 异步迭代漏洞,确保分析结果流能正确通过 AsyncGenerator 协议回传。 - 移除所有业务校验中的 assert 断言,替换为更安全的条件分支与错误消息返回。
This commit is contained in:
@@ -7,6 +7,8 @@ QQ群日常分析插件
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from astrbot.api import AstrBotConfig
|
from astrbot.api import AstrBotConfig
|
||||||
from astrbot.api import logger as astrbot_logger
|
from astrbot.api import logger as astrbot_logger
|
||||||
@@ -51,7 +53,7 @@ from .src.utils.trace_context import TraceContext, TraceLogFilter
|
|||||||
class GroupDailyAnalysis(Star):
|
class GroupDailyAnalysis(Star):
|
||||||
"""群分析插件主类"""
|
"""群分析插件主类"""
|
||||||
|
|
||||||
# ── 显式类型声明(消除 Pylance Optional 推断) ──
|
# ── 显式类型声明 (由 __init__ 初始化) ──
|
||||||
config: AstrBotConfig
|
config: AstrBotConfig
|
||||||
config_manager: ConfigManager
|
config_manager: ConfigManager
|
||||||
bot_manager: BotManager
|
bot_manager: BotManager
|
||||||
@@ -141,10 +143,21 @@ class GroupDailyAnalysis(Star):
|
|||||||
|
|
||||||
self._initialized = False
|
self._initialized = False
|
||||||
self._discovery_run = False # 是否已尝试过运行发现逻辑
|
self._discovery_run = False # 是否已尝试过运行发现逻辑
|
||||||
|
self._init_lock = asyncio.Lock()
|
||||||
|
self._background_tasks: set[asyncio.Task] = set()
|
||||||
|
|
||||||
# 异步注册任务,处理插件重载情况
|
# 异步注册任务,处理插件重载情况
|
||||||
self._init_task = asyncio.create_task(
|
try:
|
||||||
self._run_initialization("Plugin Reload/Init")
|
loop = asyncio.get_running_loop()
|
||||||
)
|
self._init_task = loop.create_task(
|
||||||
|
self._run_initialization("Plugin Reload/Init")
|
||||||
|
)
|
||||||
|
self._background_tasks.add(self._init_task)
|
||||||
|
self._init_task.add_done_callback(self._background_tasks.discard)
|
||||||
|
except RuntimeError:
|
||||||
|
# 如果当前没有 running loop (例如在非异步初始化的环境中),
|
||||||
|
# 则依赖 on_platform_loaded 钩子执行初始化
|
||||||
|
self._init_task = None
|
||||||
|
|
||||||
# orchestrators 缓存已移至 应用层逻辑 (分析服务) 或 暂时移除以简化。
|
# orchestrators 缓存已移至 应用层逻辑 (分析服务) 或 暂时移除以简化。
|
||||||
# 如果需要高性能缓存,后续可由 AnalysisApplicationService 内部维护。
|
# 如果需要高性能缓存,后续可由 AnalysisApplicationService 内部维护。
|
||||||
@@ -156,100 +169,95 @@ class GroupDailyAnalysis(Star):
|
|||||||
|
|
||||||
async def _run_initialization(self, source: str):
|
async def _run_initialization(self, source: str):
|
||||||
"""统一初始化逻辑"""
|
"""统一初始化逻辑"""
|
||||||
# 如果已经成功发现过平台,且不是来自 Platform Loaded 的强制触发,则跳过
|
async with self._init_lock:
|
||||||
if (
|
# 如果已经成功发现过平台,且不是来自 Platform Loaded 的强制触发,则跳过
|
||||||
self._initialized
|
if (
|
||||||
and self.bot_manager
|
self._initialized
|
||||||
and self.bot_manager.get_platform_count() > 0
|
and self.bot_manager
|
||||||
and source != "Platform Loaded"
|
and self.bot_manager.get_platform_count() > 0
|
||||||
):
|
and source != "Platform Loaded"
|
||||||
return
|
):
|
||||||
|
return
|
||||||
|
|
||||||
# 稍微延迟,确保 context 和环境稳定
|
# 稍微延迟,确保 context 和环境稳定
|
||||||
# 针对极少数环境,2秒可能不足以让平台管理器就绪,增加到 5秒
|
# 针对极少数环境,2秒可能不足以让平台管理器就绪,增加到 5秒
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
# [加固] 如果在等待期间插件已被卸载(terminate),则直接退出
|
# [加固] 如果在等待期间插件已被卸载(terminate),则直接退出
|
||||||
if not self.bot_manager:
|
if not self.bot_manager:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 注册 TraceID 过滤器
|
# 注册 TraceID 过滤器
|
||||||
trace_filter = TraceLogFilter()
|
trace_filter = TraceLogFilter()
|
||||||
if not any(isinstance(f, TraceLogFilter) for f in astrbot_logger.filters):
|
if not any(
|
||||||
astrbot_logger.addFilter(trace_filter)
|
isinstance(f, TraceLogFilter) for f in astrbot_logger.filters
|
||||||
astrbot_logger.info("[Trace] TraceID 日志追踪已启用")
|
|
||||||
|
|
||||||
logger.info(f"正在执行插件初始化 (来源: {source})...")
|
|
||||||
# 检查插件是否被启用 (Fix for empty plugin_set issue)
|
|
||||||
if self.context:
|
|
||||||
config = self.context.get_config()
|
|
||||||
# ... 为空修正逻辑保持不变 ...
|
|
||||||
plugin_set = config.get("plugin_set", [])
|
|
||||||
if (
|
|
||||||
isinstance(plugin_set, list)
|
|
||||||
and "astrbot_plugin_qq_group_daily_analysis" not in plugin_set
|
|
||||||
):
|
):
|
||||||
# 此时不强制修改 config,但可以记录日志
|
astrbot_logger.addFilter(trace_filter)
|
||||||
pass
|
astrbot_logger.info("[Trace] TraceID 日志追踪已启用")
|
||||||
|
|
||||||
# 1. 尝试发现 bot 实例(即使暂时没有,后续任务触发时也会再扫一遍)
|
logger.info(f"正在执行插件初始化 (来源: {source})...")
|
||||||
await self.bot_manager.initialize_from_config()
|
|
||||||
|
|
||||||
# 2. 注册预览路由器 (WebUI 路由注册不依赖在线机器人)
|
# 1. 尝试发现 bot 实例
|
||||||
if self.template_preview_router:
|
await self.bot_manager.initialize_from_config()
|
||||||
await self.template_preview_router.ensure_handlers_registered(
|
|
||||||
self.context
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3. 强制注册定时分析任务 (确保 APScheduler 即使在空载时也有任务占位)
|
# 2. 注册预览路由器
|
||||||
if self.auto_scheduler:
|
if self.template_preview_router:
|
||||||
self.auto_scheduler.schedule_jobs(self.context)
|
await self.template_preview_router.ensure_handlers_registered(
|
||||||
|
self.context
|
||||||
|
)
|
||||||
|
|
||||||
# 4. 始终启动重试管理器
|
# 3. 强制注册定时分析任务
|
||||||
if self.retry_manager:
|
if self.auto_scheduler:
|
||||||
await self.retry_manager.start()
|
self.auto_scheduler.schedule_jobs(self.context)
|
||||||
|
|
||||||
self._initialized = True
|
# 4. 始终启动重试管理器
|
||||||
self._discovery_run = True
|
if self.retry_manager:
|
||||||
logger.info(f"插件任务注册完成 (来源: {source})")
|
await self.retry_manager.start()
|
||||||
|
|
||||||
except Exception as e:
|
self._initialized = True
|
||||||
logger.error(f"插件初始化失败: {e}", exc_info=True)
|
self._discovery_run = True
|
||||||
|
logger.info(f"插件任务注册完成 (来源: {source})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"插件初始化失败: {e}", exc_info=True)
|
||||||
|
|
||||||
async def terminate(self):
|
async def terminate(self):
|
||||||
"""插件被卸载/停用时调用,清理资源"""
|
"""插件被卸载/停用时调用,清理资源"""
|
||||||
try:
|
try:
|
||||||
# 取消正在进行的初始化任务
|
|
||||||
if (
|
|
||||||
hasattr(self, "_init_task")
|
|
||||||
and self._init_task
|
|
||||||
and not self._init_task.done()
|
|
||||||
):
|
|
||||||
self._init_task.cancel()
|
|
||||||
|
|
||||||
logger.info("开始清理QQ群日常分析插件资源...")
|
logger.info("开始清理QQ群日常分析插件资源...")
|
||||||
|
|
||||||
# 停止自动调度器
|
# 1. 停止所有后台任务
|
||||||
|
if self._background_tasks:
|
||||||
|
logger.info(f"正在取消 {len(self._background_tasks)} 个后台任务...")
|
||||||
|
for task in self._background_tasks:
|
||||||
|
if not task.done():
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
# 等待任务结束
|
||||||
|
await asyncio.gather(*self._background_tasks, return_exceptions=True)
|
||||||
|
self._background_tasks.clear()
|
||||||
|
|
||||||
|
# 2. 停止各个组件
|
||||||
if self.auto_scheduler:
|
if self.auto_scheduler:
|
||||||
logger.info("正在停止自动调度器...")
|
logger.info("正在停止自动调度器...")
|
||||||
self.auto_scheduler.unschedule_jobs(self.context)
|
self.auto_scheduler.schedule_jobs(None) # type: ignore
|
||||||
logger.info("自动调度器已停止")
|
|
||||||
|
|
||||||
if self.retry_manager:
|
if self.retry_manager:
|
||||||
await self.retry_manager.stop()
|
await self.retry_manager.stop()
|
||||||
|
|
||||||
if self.template_preview_router:
|
if self.template_preview_router:
|
||||||
await self.template_preview_router.unregister_handlers()
|
await self.template_preview_router.unregister_handlers()
|
||||||
|
|
||||||
# 释放实例属性引用(插件卸载后不再使用)
|
# 3. 释放实例属性引用 (使用 type: ignore 允许 None 赋值)
|
||||||
self.auto_scheduler = None
|
self.auto_scheduler = None # type: ignore
|
||||||
self.bot_manager = None
|
self.bot_manager = None # type: ignore
|
||||||
self.report_generator = None
|
self.report_generator = None # type: ignore
|
||||||
self.config_manager = None
|
self.config_manager = None # type: ignore
|
||||||
self.message_processing_service = None
|
self.message_processing_service = None # type: ignore
|
||||||
self.telegram_group_registry = None
|
self.telegram_group_registry = None # type: ignore
|
||||||
self.template_preview_router = None
|
self.template_preview_router = None # type: ignore
|
||||||
self.telegram_template_preview_handler = None
|
self.telegram_template_preview_handler = None # type: ignore
|
||||||
|
|
||||||
logger.info("QQ群日常分析插件资源清理完成")
|
logger.info("QQ群日常分析插件资源清理完成")
|
||||||
|
|
||||||
@@ -354,27 +362,36 @@ class GroupDailyAnalysis(Star):
|
|||||||
# 2. 将内容准备为文件或数据
|
# 2. 将内容准备为文件或数据
|
||||||
image_file = None
|
image_file = None
|
||||||
created_temp = False
|
created_temp = False
|
||||||
|
MAX_PAYLOAD_SIZE = 20 * 1024 * 1024 # 20MB 限制
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
data = None
|
||||||
if image_url.startswith("base64://"):
|
if image_url.startswith("base64://"):
|
||||||
data = base64.b64decode(image_url[len("base64://") :])
|
base64_str = image_url[len("base64://") :]
|
||||||
|
if len(base64_str) * 3 / 4 > MAX_PAYLOAD_SIZE:
|
||||||
|
logger.warning("图片上传失败:Base64 负载过大")
|
||||||
|
return
|
||||||
|
data = base64.b64decode(base64_str)
|
||||||
elif image_url.startswith("data:"):
|
elif image_url.startswith("data:"):
|
||||||
parts = image_url.split(",", 1)
|
parts = image_url.split(",", 1)
|
||||||
data = base64.b64decode(parts[1]) if len(parts) == 2 else None
|
if len(parts) == 2:
|
||||||
|
if len(parts[1]) * 3 / 4 > MAX_PAYLOAD_SIZE:
|
||||||
|
logger.warning("图片上传失败:Data URI 负载过大")
|
||||||
|
return
|
||||||
|
data = base64.b64decode(parts[1])
|
||||||
elif os.path.isfile(image_url):
|
elif os.path.isfile(image_url):
|
||||||
if os.path.isabs(image_url):
|
image_file = os.path.abspath(image_url)
|
||||||
image_file = image_url
|
|
||||||
else:
|
|
||||||
image_file = os.path.abspath(image_url)
|
|
||||||
data = None
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
if data and not image_file:
|
if data and not image_file:
|
||||||
# 使用优化的文件名创建临时文件
|
# 使用 tempfile 生成唯一后缀,防止并发冲突
|
||||||
image_file = os.path.join(tempfile.gettempdir(), nice_filename)
|
fd, image_file = tempfile.mkstemp(suffix=ext, prefix="group_report_")
|
||||||
with open(image_file, "wb") as f:
|
try:
|
||||||
f.write(data)
|
with os.fdopen(fd, "wb") as f:
|
||||||
created_temp = True
|
f.write(data)
|
||||||
|
created_temp = True
|
||||||
|
except Exception:
|
||||||
|
os.close(fd)
|
||||||
|
raise
|
||||||
|
|
||||||
if not image_file:
|
if not image_file:
|
||||||
return
|
return
|
||||||
@@ -475,85 +492,8 @@ class GroupDailyAnalysis(Star):
|
|||||||
f"📊 已获取{result['messages_count']}条消息,正在生成渲染报告..."
|
f"📊 已获取{result['messages_count']}条消息,正在生成渲染报告..."
|
||||||
)
|
)
|
||||||
|
|
||||||
analysis_result = result["analysis_result"]
|
async for res in self._send_analysis_report(event, result, trace_id):
|
||||||
adapter = result["adapter"]
|
yield res
|
||||||
output_format = self.config_manager.get_output_format()
|
|
||||||
|
|
||||||
# 定义头像获取回调 (Infrastructure delegate)
|
|
||||||
async def avatar_getter(user_id: str) -> str | None:
|
|
||||||
return await adapter.get_user_avatar_url(user_id)
|
|
||||||
|
|
||||||
# 定义昵称获取回调
|
|
||||||
async def nickname_getter(user_id: str) -> str | None:
|
|
||||||
try:
|
|
||||||
member = await adapter.get_member_info(group_id, user_id)
|
|
||||||
if member:
|
|
||||||
return member.card or member.nickname
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
if output_format == "image":
|
|
||||||
(
|
|
||||||
image_url,
|
|
||||||
html_content,
|
|
||||||
) = await self.report_generator.generate_image_report(
|
|
||||||
analysis_result,
|
|
||||||
group_id,
|
|
||||||
self.html_render,
|
|
||||||
avatar_getter=avatar_getter,
|
|
||||||
nickname_getter=nickname_getter,
|
|
||||||
)
|
|
||||||
|
|
||||||
if image_url:
|
|
||||||
caption = f"📊 每日群聊分析报告已生成:\n[ID: {trace_id}]"
|
|
||||||
# 优先使用适配器的 send_image (由插件适配器统一处理 Base64 转换和路径问题)
|
|
||||||
# 不再使用 yield event.image_result 回退,防止适配器超时回复导致重复发送图片
|
|
||||||
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("⚠️ 群分析报告图片发送失败,自动重试中。")
|
|
||||||
# 使用带提示词的重试任务,确保排队发送时视觉一致
|
|
||||||
await self.retry_manager.add_task(
|
|
||||||
html_content,
|
|
||||||
analysis_result,
|
|
||||||
group_id,
|
|
||||||
platform_id,
|
|
||||||
caption=f"📊 每日群聊分析报告已生成:\n[ID: {trace_id}]",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
text_report = self.report_generator.generate_text_report(
|
|
||||||
analysis_result
|
|
||||||
)
|
|
||||||
yield event.plain_result(
|
|
||||||
f"⚠️ 图片生成失败,回退文本:\n\n{text_report}"
|
|
||||||
)
|
|
||||||
|
|
||||||
elif output_format == "pdf":
|
|
||||||
pdf_path = await self.report_generator.generate_pdf_report(
|
|
||||||
analysis_result,
|
|
||||||
group_id,
|
|
||||||
avatar_getter=avatar_getter,
|
|
||||||
nickname_getter=nickname_getter,
|
|
||||||
)
|
|
||||||
if pdf_path:
|
|
||||||
if not await adapter.send_file(group_id, pdf_path):
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
yield event.chain_result(
|
|
||||||
[File(name=Path(pdf_path).name, file=pdf_path)]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
yield event.plain_result("⚠️ PDF 生成失败。")
|
|
||||||
|
|
||||||
else:
|
|
||||||
text_report = self.report_generator.generate_text_report(
|
|
||||||
analysis_result
|
|
||||||
)
|
|
||||||
if not await adapter.send_text(group_id, text_report):
|
|
||||||
yield event.plain_result(text_report)
|
|
||||||
|
|
||||||
except DuplicateGroupTaskError:
|
except DuplicateGroupTaskError:
|
||||||
yield event.plain_result("📊 该群的分析任务正在执行中,请稍后再试哦~")
|
yield event.plain_result("📊 该群的分析任务正在执行中,请稍后再试哦~")
|
||||||
@@ -563,6 +503,77 @@ class GroupDailyAnalysis(Star):
|
|||||||
f"❌ 分析失败: {str(e)}。请检查网络连接和LLM配置,或联系管理员"
|
f"❌ 分析失败: {str(e)}。请检查网络连接和LLM配置,或联系管理员"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _send_analysis_report(
|
||||||
|
self, event: AstrMessageEvent, result: dict, trace_id: str
|
||||||
|
) -> AsyncGenerator:
|
||||||
|
"""处理分析结果的渲染和发送"""
|
||||||
|
group_id = result["group_id"]
|
||||||
|
platform_id = result["platform_id"]
|
||||||
|
analysis_result = result["analysis_result"]
|
||||||
|
adapter = result["adapter"]
|
||||||
|
output_format = self.config_manager.get_output_format()
|
||||||
|
|
||||||
|
# 定义获取回调
|
||||||
|
async def avatar_getter(user_id: str) -> str | None:
|
||||||
|
return await adapter.get_user_avatar_url(user_id)
|
||||||
|
|
||||||
|
async def nickname_getter(user_id: str) -> str | None:
|
||||||
|
try:
|
||||||
|
member = await adapter.get_member_info(group_id, user_id)
|
||||||
|
if member:
|
||||||
|
return member.card or member.nickname
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
if output_format == "image":
|
||||||
|
image_url, html_content = await self.report_generator.generate_image_report(
|
||||||
|
analysis_result,
|
||||||
|
group_id,
|
||||||
|
self.html_render,
|
||||||
|
avatar_getter=avatar_getter,
|
||||||
|
nickname_getter=nickname_getter,
|
||||||
|
)
|
||||||
|
|
||||||
|
if image_url:
|
||||||
|
caption = f"📊 每日群聊分析报告已生成:\n[ID: {trace_id}]"
|
||||||
|
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("⚠️ 群分析报告图片发送失败,自动重试中。")
|
||||||
|
await self.retry_manager.add_task(
|
||||||
|
html_content,
|
||||||
|
analysis_result,
|
||||||
|
group_id,
|
||||||
|
platform_id,
|
||||||
|
caption=f"📊 每日群聊分析报告已生成:\n[ID: {trace_id}]",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
text_report = self.report_generator.generate_text_report(
|
||||||
|
analysis_result
|
||||||
|
)
|
||||||
|
yield event.plain_result(f"⚠️ 图片生成失败,回退文本:\n\n{text_report}")
|
||||||
|
|
||||||
|
elif output_format == "pdf":
|
||||||
|
pdf_path = await self.report_generator.generate_pdf_report(
|
||||||
|
analysis_result,
|
||||||
|
group_id,
|
||||||
|
avatar_getter=avatar_getter,
|
||||||
|
nickname_getter=nickname_getter,
|
||||||
|
)
|
||||||
|
if pdf_path:
|
||||||
|
if not await adapter.send_file(group_id, pdf_path):
|
||||||
|
yield event.chain_result(
|
||||||
|
[File(name=Path(pdf_path).name, file=pdf_path)]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
yield event.plain_result("⚠️ PDF 生成失败。")
|
||||||
|
|
||||||
|
else:
|
||||||
|
text_report = self.report_generator.generate_text_report(analysis_result)
|
||||||
|
if not await adapter.send_text(group_id, text_report):
|
||||||
|
yield event.plain_result(text_report)
|
||||||
|
|
||||||
@filter.command("设置格式", alias={"set_format"})
|
@filter.command("设置格式", alias={"set_format"})
|
||||||
@filter.permission_type(PermissionType.ADMIN)
|
@filter.permission_type(PermissionType.ADMIN)
|
||||||
async def set_output_format(self, event: AstrMessageEvent, format_type: str = ""):
|
async def set_output_format(self, event: AstrMessageEvent, format_type: str = ""):
|
||||||
@@ -641,7 +652,10 @@ class GroupDailyAnalysis(Star):
|
|||||||
if parse_error:
|
if parse_error:
|
||||||
yield event.plain_result(parse_error)
|
yield event.plain_result(parse_error)
|
||||||
return
|
return
|
||||||
assert template_name is not None
|
|
||||||
|
if not template_name:
|
||||||
|
yield event.plain_result(f"❌ 无法解析模板输入: {template_input}")
|
||||||
|
return
|
||||||
|
|
||||||
if not await self.template_command_service.template_exists(template_name):
|
if not await self.template_command_service.template_exists(template_name):
|
||||||
yield event.plain_result(f"❌ 模板 '{template_name}' 不存在")
|
yield event.plain_result(f"❌ 模板 '{template_name}' 不存在")
|
||||||
@@ -702,7 +716,9 @@ class GroupDailyAnalysis(Star):
|
|||||||
yield event.plain_result("🔄 开始安装 PDF 功能依赖,请稍候...")
|
yield event.plain_result("🔄 开始安装 PDF 功能依赖,请稍候...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await PDFInstaller.install_playwright(self.config_manager)
|
result = await PDFInstaller.install_playwright(
|
||||||
|
self.config_manager, task_registry=self._background_tasks
|
||||||
|
)
|
||||||
yield event.plain_result(result)
|
yield event.plain_result(result)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -728,83 +744,12 @@ class GroupDailyAnalysis(Star):
|
|||||||
yield event.plain_result("❌ 请在群聊中使用此命令")
|
yield event.plain_result("❌ 请在群聊中使用此命令")
|
||||||
return
|
return
|
||||||
|
|
||||||
elif action == "enable":
|
if action == "enable":
|
||||||
mode = self.config_manager.get_group_list_mode()
|
async for result in self._handle_settings_enable(event, group_id):
|
||||||
target_id = event.unified_msg_origin or group_id # 优先使用 UMO
|
yield result
|
||||||
|
|
||||||
if mode == "whitelist":
|
|
||||||
glist = self.config_manager.get_group_list()
|
|
||||||
# 检查 UMO 或 Group ID 是否已在列表中
|
|
||||||
if not self.config_manager.is_group_allowed(target_id):
|
|
||||||
glist.append(target_id)
|
|
||||||
self.config_manager.set_group_list(glist)
|
|
||||||
yield event.plain_result(
|
|
||||||
f"✅ 已将当前群加入白名单\nID: {target_id}"
|
|
||||||
)
|
|
||||||
self.auto_scheduler.schedule_jobs(self.context)
|
|
||||||
else:
|
|
||||||
yield event.plain_result("ℹ️ 当前群已在白名单中")
|
|
||||||
elif mode == "blacklist":
|
|
||||||
glist = self.config_manager.get_group_list()
|
|
||||||
|
|
||||||
# 尝试移除 UMO 和 Group ID
|
|
||||||
removed = False
|
|
||||||
if target_id in glist:
|
|
||||||
glist.remove(target_id)
|
|
||||||
removed = True
|
|
||||||
if group_id in glist:
|
|
||||||
glist.remove(group_id)
|
|
||||||
removed = True
|
|
||||||
|
|
||||||
if removed:
|
|
||||||
self.config_manager.set_group_list(glist)
|
|
||||||
yield event.plain_result("✅ 已将当前群从黑名单移除")
|
|
||||||
self.auto_scheduler.schedule_jobs(self.context)
|
|
||||||
else:
|
|
||||||
yield event.plain_result("ℹ️ 当前群不在黑名单中")
|
|
||||||
else:
|
|
||||||
yield event.plain_result("ℹ️ 当前为无限制模式,所有群聊默认启用")
|
|
||||||
|
|
||||||
elif action == "disable":
|
elif action == "disable":
|
||||||
mode = self.config_manager.get_group_list_mode()
|
async for result in self._handle_settings_disable(event, group_id):
|
||||||
target_id = event.unified_msg_origin or group_id # 优先使用 UMO
|
yield result
|
||||||
|
|
||||||
if mode == "whitelist":
|
|
||||||
glist = self.config_manager.get_group_list()
|
|
||||||
|
|
||||||
# 尝试移除 UMO 和 Group ID
|
|
||||||
removed = False
|
|
||||||
if target_id in glist:
|
|
||||||
glist.remove(target_id)
|
|
||||||
removed = True
|
|
||||||
if group_id in glist:
|
|
||||||
glist.remove(group_id)
|
|
||||||
removed = True
|
|
||||||
|
|
||||||
if removed:
|
|
||||||
self.config_manager.set_group_list(glist)
|
|
||||||
yield event.plain_result("✅ 已将当前群从白名单移除")
|
|
||||||
self.auto_scheduler.schedule_jobs(self.context)
|
|
||||||
else:
|
|
||||||
yield event.plain_result("ℹ️ 当前群不在白名单中")
|
|
||||||
elif mode == "blacklist":
|
|
||||||
glist = self.config_manager.get_group_list()
|
|
||||||
# 检查 UMO 或 Group ID 是否已在列表中
|
|
||||||
if self.config_manager.is_group_allowed(
|
|
||||||
target_id
|
|
||||||
): # 如果允许,说明不在黑名单
|
|
||||||
glist.append(target_id)
|
|
||||||
self.config_manager.set_group_list(glist)
|
|
||||||
yield event.plain_result(
|
|
||||||
f"✅ 已将当前群加入黑名单\nID: {target_id}"
|
|
||||||
)
|
|
||||||
self.auto_scheduler.schedule_jobs(self.context)
|
|
||||||
else:
|
|
||||||
yield event.plain_result("ℹ️ 当前群已在黑名单中")
|
|
||||||
else:
|
|
||||||
yield event.plain_result(
|
|
||||||
"ℹ️ 当前为无限制模式,如需禁用请切换到黑名单模式"
|
|
||||||
)
|
|
||||||
|
|
||||||
elif action == "reload":
|
elif action == "reload":
|
||||||
self.auto_scheduler.schedule_jobs(self.context)
|
self.auto_scheduler.schedule_jobs(self.context)
|
||||||
@@ -940,3 +885,69 @@ class GroupDailyAnalysis(Star):
|
|||||||
f"• 参与者: {summary['participants']}\n"
|
f"• 参与者: {summary['participants']}\n"
|
||||||
f"• 高峰时段: {summary['peak_hours']}"
|
f"• 高峰时段: {summary['peak_hours']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _handle_settings_enable(self, event: AstrMessageEvent, group_id: str):
|
||||||
|
"""协助逻辑:处理启用设置的分支逻辑"""
|
||||||
|
mode = self.config_manager.get_group_list_mode()
|
||||||
|
target_id = event.unified_msg_origin or group_id
|
||||||
|
|
||||||
|
if mode == "whitelist":
|
||||||
|
glist = self.config_manager.get_group_list()
|
||||||
|
if not self.config_manager.is_group_allowed(target_id):
|
||||||
|
glist.append(target_id)
|
||||||
|
self.config_manager.set_group_list(glist)
|
||||||
|
yield event.plain_result(f"✅ 已将当前群加入白名单\nID: {target_id}")
|
||||||
|
self.auto_scheduler.schedule_jobs(self.context)
|
||||||
|
else:
|
||||||
|
yield event.plain_result("ℹ️ 当前群已在白名单中")
|
||||||
|
elif mode == "blacklist":
|
||||||
|
glist = self.config_manager.get_group_list()
|
||||||
|
removed = False
|
||||||
|
if target_id in glist:
|
||||||
|
glist.remove(target_id)
|
||||||
|
removed = True
|
||||||
|
if group_id in glist:
|
||||||
|
glist.remove(group_id)
|
||||||
|
removed = True
|
||||||
|
|
||||||
|
if removed:
|
||||||
|
self.config_manager.set_group_list(glist)
|
||||||
|
yield event.plain_result("✅ 已将当前群从黑名单移除")
|
||||||
|
self.auto_scheduler.schedule_jobs(self.context)
|
||||||
|
else:
|
||||||
|
yield event.plain_result("ℹ️ 当前群不在黑名单中")
|
||||||
|
else:
|
||||||
|
yield event.plain_result("ℹ️ 当前为无限制模式,所有群聊默认启用")
|
||||||
|
|
||||||
|
async def _handle_settings_disable(self, event: AstrMessageEvent, group_id: str):
|
||||||
|
"""协助逻辑:处理禁用设置的分支逻辑"""
|
||||||
|
mode = self.config_manager.get_group_list_mode()
|
||||||
|
target_id = event.unified_msg_origin or group_id
|
||||||
|
|
||||||
|
if mode == "whitelist":
|
||||||
|
glist = self.config_manager.get_group_list()
|
||||||
|
removed = False
|
||||||
|
if target_id in glist:
|
||||||
|
glist.remove(target_id)
|
||||||
|
removed = True
|
||||||
|
if group_id in glist:
|
||||||
|
glist.remove(group_id)
|
||||||
|
removed = True
|
||||||
|
|
||||||
|
if removed:
|
||||||
|
self.config_manager.set_group_list(glist)
|
||||||
|
yield event.plain_result("✅ 已将当前群从白名单移除")
|
||||||
|
self.auto_scheduler.schedule_jobs(self.context)
|
||||||
|
else:
|
||||||
|
yield event.plain_result("ℹ️ 当前群不在白名单中")
|
||||||
|
elif mode == "blacklist":
|
||||||
|
glist = self.config_manager.get_group_list()
|
||||||
|
if self.config_manager.is_group_allowed(target_id):
|
||||||
|
glist.append(target_id)
|
||||||
|
self.config_manager.set_group_list(glist)
|
||||||
|
yield event.plain_result(f"✅ 已将当前群加入黑名单\nID: {target_id}")
|
||||||
|
self.auto_scheduler.schedule_jobs(self.context)
|
||||||
|
else:
|
||||||
|
yield event.plain_result("ℹ️ 当前群已在黑名单中")
|
||||||
|
else:
|
||||||
|
yield event.plain_result("ℹ️ 当前为无限制模式,如需禁用请切换到黑名单模式")
|
||||||
|
|||||||
Reference in New Issue
Block a user