Files
Mofox-Core/src/schedule/schedule_manager.py
Windpicker-owo d21ac89617 fix: 修复数据库导入错误
- 修复12处从错误位置导入get_db_session/get_engine的问题
- 函数应从core导入,模型应从core.models导入
- 修复bot.py使用check_and_migrate_database代替不存在的initialize_database

影响文件:
- bot.py: 使用正确的初始化函数
- schedule_manager.py: 修复get_db_session导入
- relationship_service.py: 修复get_db_session导入
- schedule_api.py: 修复get_db_session导入
- utils.py (llm_models): 修复get_db_session导入
- utils_image.py: 修复get_db_session导入
- utils_video.py: 修复get_db_session导入
- user_ban.py: 修复get_db_session导入
- statistics.py: 修复get_db_session导入
- storage.py: 修复get_db_session导入
- anti_injector.py: 修复2处get_db_session导入
- permission_manager.py: 修复get_engine导入
2025-11-19 23:30:55 +08:00

333 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
from datetime import datetime, time, timedelta
from typing import Any
import orjson
from sqlalchemy import select
from src.common.database.core.models import MonthlyPlan, Schedule
from src.common.database.core import get_db_session
from src.common.logger import get_logger
from src.config.config import global_config
from src.manager.async_task_manager import AsyncTask, async_task_manager
from .database import update_plan_usage
from .llm_generator import ScheduleLLMGenerator
from .plan_manager import PlanManager
from .schemas import ScheduleData
logger = get_logger("schedule_manager")
class ScheduleManager:
"""
负责管理每日日程的核心类。
它处理日程的加载、生成、保存以及提供当前活动查询等功能。
"""
def __init__(self):
"""
初始化 ScheduleManager。
"""
self.today_schedule: list[dict[str, Any]] | None = None # 存储当天的日程数据
self.llm_generator = ScheduleLLMGenerator() # 用于生成日程的LLM生成器实例
self.plan_manager = PlanManager() # 月度计划管理器实例
self.daily_task_started = False # 标记每日自动生成任务是否已启动
self.schedule_generation_running = False # 标记当前是否有日程生成任务正在运行,防止重复执行
async def initialize(self):
"""
异步初始化日程管理器。
如果日程功能已启用,则会加载或生成当天的日程,并启动每日自动生成任务。
"""
if global_config.planning_system.schedule_enable:
logger.info("日程表功能已启用,正在初始化管理器...")
await self.load_or_generate_today_schedule()
await self.start_daily_schedule_generation()
logger.info("日程表管理器初始化成功。")
async def start_daily_schedule_generation(self):
"""
启动一个后台任务,该任务会在每天零点自动生成第二天的日程。
"""
if not self.daily_task_started:
logger.info("正在启动每日日程生成任务...")
task = DailyScheduleGenerationTask(self)
await async_task_manager.add_task(task)
self.daily_task_started = True
logger.info("每日日程生成任务已成功启动。")
else:
logger.info("每日日程生成任务已在运行中。")
async def load_or_generate_today_schedule(self):
"""
加载或生成当天的日程。
首先尝试从数据库加载如果失败或不存在则调用LLM生成新的日程。
"""
if not global_config.planning_system.schedule_enable:
logger.info("日程管理功能已禁用,跳过日程加载和生成。")
return
today_str = datetime.now().strftime("%Y-%m-%d")
try:
# 尝试从数据库加载日程
schedule_data = await self._load_schedule_from_db(today_str)
if schedule_data:
self.today_schedule = schedule_data
self._log_loaded_schedule(today_str)
return
# 如果数据库中没有,则生成新的日程
logger.info(f"数据库中未找到今天的日程 ({today_str}),将调用 LLM 生成。")
await self.generate_and_save_schedule()
except Exception as e:
# 如果加载过程中出现任何异常,则尝试生成日程作为备用方案
logger.error(f"加载或生成日程时出错: {e}")
logger.info("尝试生成日程作为备用方案...")
await self.generate_and_save_schedule()
async def _load_schedule_from_db(self, date_str: str) -> list[dict[str, Any]] | None:
"""
从数据库中加载指定日期的日程。
Args:
date_str (str): 日期字符串,格式为 "YYYY-MM-DD"
Returns:
list[dict[str, Any]] | None: 如果找到并验证成功,则返回日程数据,否则返回 None。
"""
async with get_db_session() as session:
result = await session.execute(select(Schedule).filter(Schedule.date == date_str))
schedule_record = result.scalars().first()
if schedule_record:
logger.info(f"从数据库加载今天的日程 ({date_str})。")
schedule_data = orjson.loads(str(schedule_record.schedule_data))
# 验证数据格式是否符合 Pydantic 模型
if self._validate_schedule_with_pydantic(schedule_data):
return schedule_data
else:
logger.warning("数据库中的日程数据格式无效,将重新生成日程")
return None
def _log_loaded_schedule(self, date_str: str):
"""
记录已成功加载的日程信息。
Args:
date_str (str): 日期字符串。
"""
schedule_str = f"已成功加载今天的日程 ({date_str})\n"
if self.today_schedule:
for item in self.today_schedule:
schedule_str += f" - {item.get('time_range', '未知时间')}: {item.get('activity', '未知活动')}\n"
logger.info(schedule_str)
async def generate_and_save_schedule(self):
"""
提交一个按需生成的后台任务来创建和保存日程。
这种设计可以防止在主流程中长时间等待LLM响应。
"""
if self.schedule_generation_running:
logger.info("日程生成任务已在运行中,跳过重复启动")
return
logger.info("检测到需要生成日程,已提交后台任务。")
task = OnDemandScheduleGenerationTask(self)
await async_task_manager.add_task(task)
async def _async_generate_and_save_schedule(self):
"""
实际执行日程生成和保存的异步方法。
这个方法由后台任务调用。
"""
self.schedule_generation_running = True
try:
today_str = datetime.now().strftime("%Y-%m-%d")
current_month_str = datetime.now().strftime("%Y-%m")
# 如果启用了月度计划,则获取一些计划作为生成日程的参考
sampled_plans = []
if global_config.planning_system.monthly_plan_enable:
await self.plan_manager.ensure_and_generate_plans_if_needed(current_month_str)
sampled_plans = await self.plan_manager.get_plans_for_schedule(current_month_str, max_count=3)
# 调用LLM生成日程数据
schedule_data = await self.llm_generator.generate_schedule_with_llm(sampled_plans)
if schedule_data:
# 保存到数据库
await self._save_schedule_to_db(today_str, schedule_data)
self.today_schedule = schedule_data
self._log_generated_schedule(today_str, schedule_data, sampled_plans)
# 如果参考了月度计划,则更新这些计划的使用情况
if sampled_plans:
used_plan_ids = [plan.id for plan in sampled_plans]
logger.info(f"更新使用过的月度计划 {used_plan_ids} 的统计信息。")
await update_plan_usage(used_plan_ids, today_str)
finally:
self.schedule_generation_running = False
logger.info("日程生成任务结束")
@staticmethod
async def _save_schedule_to_db(date_str: str, schedule_data: list[dict[str, Any]]):
"""
将日程数据保存到数据库。如果已有记录则更新,否则创建新记录。
Args:
date_str (str): 日期字符串。
schedule_data (list[dict[str, Any]]): 日程数据。
"""
async with get_db_session() as session:
schedule_json = orjson.dumps(schedule_data).decode("utf-8")
# 查找是否已存在当天的日程记录
result = await session.execute(select(Schedule).filter(Schedule.date == date_str))
existing_schedule = result.scalars().first()
if existing_schedule:
# 更新现有记录
existing_schedule.schedule_data = schedule_json
existing_schedule.updated_at = datetime.now()
else:
# 创建新记录
new_schedule = Schedule(date=date_str, schedule_data=schedule_json)
session.add(new_schedule)
await session.commit()
@staticmethod
def _log_generated_schedule(
date_str: str, schedule_data: list[dict[str, Any]], sampled_plans: list[MonthlyPlan]
):
"""
记录成功生成并保存的日程信息。
Args:
date_str (str): 日期字符串。
schedule_data (list[dict[str, Any]]): 日程数据。
sampled_plans (list[MonthlyPlan]]): 用于生成日程的参考月度计划。
"""
schedule_str = f"成功生成并保存今天的日程 ({date_str})\n"
if sampled_plans:
plan_texts = "\n".join([f" - {plan.plan_text}" for plan in sampled_plans])
schedule_str += f"本次日程参考的月度计划:\n{plan_texts}\n"
schedule_str += "今日日程详情:\n"
for item in schedule_data:
schedule_str += f" - {item.get('time_range', '未知时间')}: {item.get('activity', '未知活动')}\n"
logger.info(schedule_str)
def get_current_activity(self) -> dict[str, Any] | None:
"""
根据当前时间从日程表中获取正在进行的活动。
Returns:
dict[str, Any] | None: 如果找到当前活动,则返回包含活动和时间范围的字典,否则返回 None。
"""
if not global_config.planning_system.schedule_enable or not self.today_schedule:
return None
now = datetime.now().time()
for event in self.today_schedule:
try:
time_range = event.get("time_range")
activity = event.get("activity")
if not time_range or not activity:
continue
# 解析时间范围
start_str, end_str = time_range.split("-")
start_time = datetime.strptime(start_str.strip(), "%H:%M").time()
end_time = datetime.strptime(end_str.strip(), "%H:%M").time()
# 判断当前时间是否在时间范围内(支持跨天的时间范围,如 23:00-01:00
if (start_time <= now < end_time) or (end_time < start_time and (now >= start_time or now < end_time)):
return {"activity": activity, "time_range": time_range}
except (ValueError, KeyError, AttributeError) as e:
logger.warning(f"解析日程事件失败: {event}, 错误: {e}")
return None
@staticmethod
def _validate_schedule_with_pydantic(schedule_data) -> bool:
"""
使用 Pydantic 模型验证日程数据的格式和内容是否正确。
Args:
schedule_data: 待验证的日程数据。
Returns:
bool: 如果验证通过则返回 True否则返回 False。
"""
try:
ScheduleData(schedule=schedule_data)
return True
except Exception:
return False
class OnDemandScheduleGenerationTask(AsyncTask):
"""
一个按需执行的后台任务,用于生成当天的日程。
当启动时未找到日程或加载失败时触发。
"""
def __init__(self, schedule_manager: "ScheduleManager"):
"""
初始化按需日程生成任务。
Args:
schedule_manager (ScheduleManager): ScheduleManager 的实例。
"""
task_name = f"OnDemandScheduleGenerationTask-{datetime.now().strftime('%Y%m%d%H%M%S')}"
super().__init__(task_name=task_name)
self.schedule_manager = schedule_manager
async def run(self):
"""
任务的执行体,调用 ScheduleManager 中的核心生成逻辑。
"""
logger.info(f"后台任务 {self.task_name} 开始执行日程生成。")
await self.schedule_manager._async_generate_and_save_schedule()
logger.info(f"后台任务 {self.task_name} 完成。")
class DailyScheduleGenerationTask(AsyncTask):
"""
一个周期性执行的后台任务,用于在每天零点自动生成新一天的日程。
"""
def __init__(self, schedule_manager: "ScheduleManager"):
"""
初始化每日日程生成任务。
Args:
schedule_manager (ScheduleManager): ScheduleManager 的实例。
"""
super().__init__(task_name="DailyScheduleGenerationTask")
self.schedule_manager = schedule_manager
async def run(self):
"""
任务的执行体,无限循环直到被取消。
计算到下一个零点的时间并休眠,然后在零点过后触发日程生成。
"""
while True:
try:
now = datetime.now()
# 计算下一个零点的时间
tomorrow = now.date() + timedelta(days=1)
midnight = datetime.combine(tomorrow, time.min)
sleep_seconds = (midnight - now).total_seconds()
logger.info(
f"下一次日程生成任务将在 {sleep_seconds:.2f} 秒后运行 (北京时间 {midnight.strftime('%Y-%m-%d %H:%M:%S')})"
)
await asyncio.sleep(sleep_seconds)
# 到达零点,开始生成
logger.info("到达每日零点,开始生成新的一天日程...")
await self.schedule_manager._async_generate_and_save_schedule()
except asyncio.CancelledError:
logger.info("每日日程生成任务被取消。")
break
except Exception as e:
# 发生未知错误时,记录日志并短暂休眠后重试,避免任务崩溃
logger.error(f"每日日程生成任务发生未知错误: {e}")
await asyncio.sleep(300)
# 创建 ScheduleManager 的单例
schedule_manager = ScheduleManager()