feat(monthly_plan): 增强月度计划系统,引入状态管理和智能抽取

对月度计划系统进行了全面的重构和功能增强,以提供更智能、更可持续的计划管理体验。

主要变更包括:
- **引入状态生命周期**: 废弃了原有的 `is_deleted` 软删除标记,引入了更明确的 `status` 字段 (`active`, `completed`, `archived`),用于管理计划的整个生命周期。
- **增加使用统计与自动完成**: 新增 `usage_count` 和 `last_used_date` 字段来跟踪计划的使用情况。当计划使用次数达到可配置的阈值后,会自动标记为 `completed`。
- **实现智能计划抽取**: 为每日日程生成实现了新的智能抽取算法。该算法会优先选择使用次数较少且近期未被使用的计划,以增加计划的多样性并避免重复。
- **更新配置选项**: 移除了旧的概率删除相关配置,增加了 `completion_threshold`、`avoid_repetition_days` 等新选项以支持新逻辑。
- **数据库模型更新**: 更新了 `MonthlyPlan` 的数据库模型和索引,以支持新功能并优化查询性能。保留 `is_deleted` 字段以兼容旧数据。
This commit is contained in:
minecraft1024a
2025-08-26 19:19:53 +08:00
parent a31df44e09
commit cbd115efdb
7 changed files with 584 additions and 50 deletions

View File

@@ -16,10 +16,10 @@ def add_new_plans(plans: List[str], month: str):
""" """
with get_db_session() as session: with get_db_session() as session:
try: try:
# 1. 获取当前有效计划数量 # 1. 获取当前有效计划数量(状态为 'active'
current_plan_count = session.query(MonthlyPlan).filter( current_plan_count = session.query(MonthlyPlan).filter(
MonthlyPlan.target_month == month, MonthlyPlan.target_month == month,
not MonthlyPlan.is_deleted MonthlyPlan.status == 'active'
).count() ).count()
# 2. 从配置获取上限 # 2. 从配置获取上限
@@ -36,7 +36,7 @@ def add_new_plans(plans: List[str], month: str):
plans_to_add = plans[:remaining_slots] plans_to_add = plans[:remaining_slots]
new_plan_objects = [ new_plan_objects = [
MonthlyPlan(plan_text=plan, target_month=month) MonthlyPlan(plan_text=plan, target_month=month, status='active')
for plan in plans_to_add for plan in plans_to_add
] ]
session.add_all(new_plan_objects) session.add_all(new_plan_objects)
@@ -53,7 +53,7 @@ def add_new_plans(plans: List[str], month: str):
def get_active_plans_for_month(month: str) -> List[MonthlyPlan]: def get_active_plans_for_month(month: str) -> List[MonthlyPlan]:
""" """
获取指定月份所有未被软删除的计划。 获取指定月份所有状态为 'active' 的计划。
:param month: 目标月份,格式为 "YYYY-MM" :param month: 目标月份,格式为 "YYYY-MM"
:return: MonthlyPlan 对象列表。 :return: MonthlyPlan 对象列表。
@@ -62,18 +62,18 @@ def get_active_plans_for_month(month: str) -> List[MonthlyPlan]:
try: try:
plans = session.query(MonthlyPlan).filter( plans = session.query(MonthlyPlan).filter(
MonthlyPlan.target_month == month, MonthlyPlan.target_month == month,
not MonthlyPlan.is_deleted MonthlyPlan.status == 'active'
).all() ).all()
return plans return plans
except Exception as e: except Exception as e:
logger.error(f"查询 {month} 的有效月度计划时发生错误: {e}") logger.error(f"查询 {month} 的有效月度计划时发生错误: {e}")
return [] return []
def soft_delete_plans(plan_ids: List[int]): def mark_plans_completed(plan_ids: List[int]):
""" """
将指定ID的计划标记为软删除 将指定ID的计划标记为已完成
:param plan_ids: 需要软删除的计划ID列表。 :param plan_ids: 需要标记为完成的计划ID列表。
""" """
if not plan_ids: if not plan_ids:
return return
@@ -82,10 +82,178 @@ def soft_delete_plans(plan_ids: List[int]):
try: try:
session.query(MonthlyPlan).filter( session.query(MonthlyPlan).filter(
MonthlyPlan.id.in_(plan_ids) MonthlyPlan.id.in_(plan_ids)
).update({"is_deleted": True}, synchronize_session=False) ).update({"status": "completed"}, synchronize_session=False)
session.commit() session.commit()
logger.info(f"成功软删除了 {len(plan_ids)} 条月度计划。") logger.info(f"成功 {len(plan_ids)} 条月度计划标记为已完成")
except Exception as e: except Exception as e:
logger.error(f"软删除月度计划时发生错误: {e}") logger.error(f"标记月度计划为完成时发生错误: {e}")
session.rollback() session.rollback()
raise raise
def soft_delete_plans(plan_ids: List[int]):
"""
将指定ID的计划标记为软删除兼容旧接口
现在实际上是标记为已完成。
:param plan_ids: 需要软删除的计划ID列表。
"""
logger.warning("soft_delete_plans 已弃用,请使用 mark_plans_completed")
mark_plans_completed(plan_ids)
def update_plan_usage(plan_ids: List[int], used_date: str):
"""
更新计划的使用统计信息。
:param plan_ids: 使用的计划ID列表。
:param used_date: 使用日期,格式为 "YYYY-MM-DD"
"""
if not plan_ids:
return
with get_db_session() as session:
try:
# 获取完成阈值配置,如果不存在则使用默认值
completion_threshold = getattr(global_config.monthly_plan_system, 'completion_threshold', 3)
# 批量更新使用次数和最后使用日期
session.query(MonthlyPlan).filter(
MonthlyPlan.id.in_(plan_ids)
).update({
"usage_count": MonthlyPlan.usage_count + 1,
"last_used_date": used_date
}, synchronize_session=False)
# 检查是否有计划达到完成阈值
plans_to_complete = session.query(MonthlyPlan).filter(
MonthlyPlan.id.in_(plan_ids),
MonthlyPlan.usage_count >= completion_threshold,
MonthlyPlan.status == 'active'
).all()
if plans_to_complete:
completed_ids = [plan.id for plan in plans_to_complete]
session.query(MonthlyPlan).filter(
MonthlyPlan.id.in_(completed_ids)
).update({
"status": "completed"
}, synchronize_session=False)
logger.info(f"计划 {completed_ids} 已达到使用阈值 ({completion_threshold}),标记为已完成。")
session.commit()
logger.info(f"成功更新了 {len(plan_ids)} 条月度计划的使用统计。")
except Exception as e:
logger.error(f"更新月度计划使用统计时发生错误: {e}")
session.rollback()
raise
def get_smart_plans_for_daily_schedule(month: str, max_count: int = 3, avoid_days: int = 7) -> List[MonthlyPlan]:
"""
智能抽取月度计划用于每日日程生成。
抽取规则:
1. 避免短期内重复avoid_days 天内不重复抽取同一个计划)
2. 优先抽取使用次数较少的计划
3. 在满足以上条件的基础上随机抽取
:param month: 目标月份,格式为 "YYYY-MM"
:param max_count: 最多抽取的计划数量。
:param avoid_days: 避免重复的天数。
:return: MonthlyPlan 对象列表。
"""
from datetime import datetime, timedelta
with get_db_session() as session:
try:
# 计算避免重复的日期阈值
avoid_date = (datetime.now() - timedelta(days=avoid_days)).strftime("%Y-%m-%d")
# 查询符合条件的计划
query = session.query(MonthlyPlan).filter(
MonthlyPlan.target_month == month,
MonthlyPlan.status == 'active'
)
# 排除最近使用过的计划
query = query.filter(
(MonthlyPlan.last_used_date.is_(None)) |
(MonthlyPlan.last_used_date < avoid_date)
)
# 按使用次数升序排列,优先选择使用次数少的
plans = query.order_by(MonthlyPlan.usage_count.asc()).all()
if not plans:
logger.info(f"没有找到符合条件的 {month} 月度计划。")
return []
# 如果计划数量超过需要的数量,进行随机抽取
if len(plans) > max_count:
import random
plans = random.sample(plans, max_count)
logger.info(f"智能抽取了 {len(plans)}{month} 的月度计划用于每日日程生成。")
return plans
except Exception as e:
logger.error(f"智能抽取 {month} 的月度计划时发生错误: {e}")
return []
def archive_active_plans_for_month(month: str):
"""
将指定月份所有状态为 'active' 的计划归档为 'archived'
通常在月底调用。
:param month: 目标月份,格式为 "YYYY-MM"
"""
with get_db_session() as session:
try:
updated_count = session.query(MonthlyPlan).filter(
MonthlyPlan.target_month == month,
MonthlyPlan.status == 'active'
).update({"status": "archived"}, synchronize_session=False)
session.commit()
logger.info(f"成功将 {updated_count}{month} 的活跃月度计划归档。")
return updated_count
except Exception as e:
logger.error(f"归档 {month} 的月度计划时发生错误: {e}")
session.rollback()
raise
def get_archived_plans_for_month(month: str) -> List[MonthlyPlan]:
"""
获取指定月份所有状态为 'archived' 的计划。
用于生成下个月计划时的参考。
:param month: 目标月份,格式为 "YYYY-MM"
:return: MonthlyPlan 对象列表。
"""
with get_db_session() as session:
try:
plans = session.query(MonthlyPlan).filter(
MonthlyPlan.target_month == month,
MonthlyPlan.status == 'archived'
).all()
return plans
except Exception as e:
logger.error(f"查询 {month} 的归档月度计划时发生错误: {e}")
return []
def has_active_plans(month: str) -> bool:
"""
检查指定月份是否存在任何状态为 'active' 的计划。
:param month: 目标月份,格式为 "YYYY-MM"
:return: 如果存在则返回 True否则返回 False。
"""
with get_db_session() as session:
try:
count = session.query(MonthlyPlan).filter(
MonthlyPlan.target_month == month,
MonthlyPlan.status == 'active'
).count()
return count > 0
except Exception as e:
logger.error(f"检查 {month} 的有效月度计划时发生错误: {e}")
return False

View File

@@ -5,11 +5,12 @@
from sqlalchemy import Column, String, Float, Integer, Boolean, Text, Index, create_engine, DateTime from sqlalchemy import Column, String, Float, Integer, Boolean, Text, Index, create_engine, DateTime
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.pool import QueuePool from sqlalchemy.pool import QueuePool
import os import os
import datetime import datetime
import time import time
from typing import Iterator, Optional
from src.common.logger import get_logger from src.common.logger import get_logger
from contextlib import contextmanager from contextlib import contextmanager
@@ -508,16 +509,25 @@ class CacheEntries(Base):
) )
class MonthlyPlan(Base): class MonthlyPlan(Base):
"""计划模型""" """计划模型"""
__tablename__ = 'monthly_plans' __tablename__ = 'monthly_plans'
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
plan_text = Column(Text, nullable=False) plan_text = Column(Text, nullable=False)
target_month = Column(String(7), nullable=False, index=True) # "YYYY-MM" target_month = Column(String(7), nullable=False, index=True) # "YYYY-MM"
is_deleted = Column(Boolean, nullable=False, default=False, index=True) status = Column(get_string_field(20), nullable=False, default='active', index=True) # 'active', 'completed', 'archived'
usage_count = Column(Integer, nullable=False, default=0)
last_used_date = Column(String(10), nullable=True, index=True) # "YYYY-MM-DD" format
created_at = Column(DateTime, nullable=False, default=datetime.datetime.now) created_at = Column(DateTime, nullable=False, default=datetime.datetime.now)
# 保留 is_deleted 字段以兼容现有数据,但标记为已弃用
is_deleted = Column(Boolean, nullable=False, default=False)
__table_args__ = ( __table_args__ = (
Index('idx_monthlyplan_target_month_status', 'target_month', 'status'),
Index('idx_monthlyplan_last_used_date', 'last_used_date'),
Index('idx_monthlyplan_usage_count', 'usage_count'),
# 保留旧索引以兼容
Index('idx_monthlyplan_target_month_is_deleted', 'target_month', 'is_deleted'), Index('idx_monthlyplan_target_month_is_deleted', 'target_month', 'is_deleted'),
) )
@@ -628,9 +638,9 @@ def initialize_database():
@contextmanager @contextmanager
def get_db_session(): def get_db_session() -> Iterator[Session]:
"""数据库会话上下文管理器 - 推荐使用这个而不是get_session()""" """数据库会话上下文管理器 - 推荐使用这个而不是get_session()"""
session = None session: Optional[Session] = None
try: try:
_, SessionLocal = initialize_database() _, SessionLocal = initialize_database()
session = SessionLocal() session = SessionLocal()

View File

@@ -680,13 +680,13 @@ class WakeUpSystemConfig(ValidatedConfigBase):
class MonthlyPlanSystemConfig(ValidatedConfigBase): class MonthlyPlanSystemConfig(ValidatedConfigBase):
"""计划系统配置类""" """计划系统配置类"""
enable: bool = Field(default=True, description="是否启用本功能") enable: bool = Field(default=True, description="是否启用本功能")
generation_threshold: int = Field(default=10, ge=0, description="启动时如果当月计划少于此数量则触发LLM生成")
plans_per_generation: int = Field(default=5, ge=1, description="每次调用LLM期望生成的计划数量")
deletion_probability_on_use: float = Field(default=0.5, ge=0.0, le=1.0, description="计划被使用后,被删除的概率")
max_plans_per_month: int = Field(default=20, ge=1, description="每个月允许存在的最大计划数量") max_plans_per_month: int = Field(default=20, ge=1, description="每个月允许存在的最大计划数量")
completion_threshold: int = Field(default=3, ge=1, description="计划使用多少次后自动标记为已完成")
avoid_repetition_days: int = Field(default=7, ge=1, description="多少天内不重复抽取同一个计划")
guidelines: Optional[str] = Field(default=None, description="月度计划生成的指导原则")
class ContextGroup(ValidatedConfigBase): class ContextGroup(ValidatedConfigBase):

View File

@@ -18,7 +18,7 @@ from src.common.server import get_global_server, Server
from src.mood.mood_manager import mood_manager from src.mood.mood_manager import mood_manager
from rich.traceback import install from rich.traceback import install
from src.manager.schedule_manager import schedule_manager from src.manager.schedule_manager import schedule_manager
from src.schedule.monthly_plan_manager import MonthlyPlanManager from src.manager.monthly_plan_manager import monthly_plan_manager
from src.plugin_system.core.event_manager import event_manager from src.plugin_system.core.event_manager import event_manager
from src.plugin_system.base.component_types import EventType from src.plugin_system.base.component_types import EventType
# from src.api.main import start_api_server # from src.api.main import start_api_server
@@ -235,14 +235,18 @@ MaiMbot-Pro-Max(第三方修改版)
# 初始化个体特征 # 初始化个体特征
await self.individuality.initialize() await self.individuality.initialize()
# 初始化月度计划管理器
if global_config.monthly_plan_system.enable:
logger.info("正在初始化月度计划管理器...")
try:
await monthly_plan_manager.start_monthly_plan_generation()
logger.info("月度计划管理器初始化成功")
except Exception as e:
logger.error(f"月度计划管理器初始化失败: {e}")
# 初始化日程管理器 # 初始化日程管理器
if global_config.schedule.enable: if global_config.schedule.enable:
logger.info("正在初始化月度计划...")
try:
await MonthlyPlanManager.initialize_monthly_plans()
logger.info("月度计划初始化完成")
except Exception as e:
logger.error(f"月度计划初始化失败: {e}")
logger.info("日程表功能已启用,正在初始化管理器...") logger.info("日程表功能已启用,正在初始化管理器...")
await schedule_manager.load_or_generate_today_schedule() await schedule_manager.load_or_generate_today_schedule()
await schedule_manager.start_daily_schedule_generation() await schedule_manager.start_daily_schedule_generation()
@@ -306,6 +310,10 @@ MaiMbot-Pro-Max(第三方修改版)
def sync_build_memory(): def sync_build_memory():
"""在线程池中执行同步记忆构建""" """在线程池中执行同步记忆构建"""
if not self.hippocampus_manager:
logger.error("尝试在禁用记忆系统时构建记忆,操作已取消。")
return
try: try:
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)

View File

@@ -0,0 +1,315 @@
# mmc/src/manager/monthly_plan_manager.py
import asyncio
from datetime import datetime, timedelta
from typing import List, Optional
import orjson
from json_repair import repair_json
from src.common.database.monthly_plan_db import (
add_new_plans,
get_archived_plans_for_month,
archive_active_plans_for_month,
has_active_plans
)
from src.config.config import global_config, model_config
from src.llm_models.utils_model import LLMRequest
from src.common.logger import get_logger
from src.manager.async_task_manager import AsyncTask, async_task_manager
logger = get_logger("monthly_plan_manager")
# 默认的月度计划生成指导原则
DEFAULT_MONTHLY_PLAN_GUIDELINES = """
我希望你能为自己制定一些有意义的月度小目标和计划。
这些计划应该涵盖学习、娱乐、社交、个人成长等各个方面。
每个计划都应该是具体可行的,能够在一个月内通过日常活动逐步实现。
请确保计划既有挑战性又不会过于繁重,保持生活的平衡和乐趣。
"""
class MonthlyPlanManager:
"""月度计划管理器
负责月度计划的生成、管理和生命周期控制。
与 ScheduleManager 解耦,专注于月度层面的计划管理。
"""
def __init__(self):
self.llm = LLMRequest(
model_set=model_config.model_task_config.schedule_generator,
request_type="monthly_plan"
)
self.generation_running = False
self.monthly_task_started = False
async def start_monthly_plan_generation(self):
"""启动每月初自动生成新月度计划的任务,并在启动时检查一次"""
if not self.monthly_task_started:
logger.info("正在启动每月月度计划生成任务...")
task = MonthlyPlanGenerationTask(self)
await async_task_manager.add_task(task)
self.monthly_task_started = True
logger.info("每月月度计划生成任务已成功启动。")
# 启动时立即检查并按需生成
logger.info("执行启动时月度计划检查...")
await self.ensure_and_generate_plans_if_needed()
else:
logger.info("每月月度计划生成任务已在运行中。")
async def ensure_and_generate_plans_if_needed(self, target_month: Optional[str] = None) -> bool:
"""
确保指定月份有计划,如果没有则触发生成。
这是按需生成的主要入口点。
"""
if target_month is None:
target_month = datetime.now().strftime("%Y-%m")
if not has_active_plans(target_month):
logger.info(f"{target_month} 没有任何有效的月度计划,将立即生成。")
return await self.generate_monthly_plans(target_month)
else:
# logger.info(f"{target_month} 已存在有效的月度计划,跳过生成。")
return True # 已经有计划,也算成功
async def generate_monthly_plans(self, target_month: Optional[str] = None) -> bool:
"""
生成指定月份的月度计划
:param target_month: 目标月份,格式为 "YYYY-MM"。如果为 None则为当前月份。
:return: 是否生成成功
"""
if self.generation_running:
logger.info("月度计划生成任务已在运行中,跳过重复启动")
return False
self.generation_running = True
try:
# 确定目标月份
if target_month is None:
target_month = datetime.now().strftime("%Y-%m")
logger.info(f"开始为 {target_month} 生成月度计划...")
# 检查是否启用月度计划系统
if not global_config.monthly_plan_system or not global_config.monthly_plan_system.enable:
logger.info("月度计划系统已禁用,跳过计划生成。")
return False
# 获取上个月的归档计划作为参考
last_month = self._get_previous_month(target_month)
archived_plans = get_archived_plans_for_month(last_month)
# 构建生成 Prompt
prompt = self._build_generation_prompt(target_month, archived_plans)
# 调用 LLM 生成计划
plans = await self._generate_plans_with_llm(prompt)
if plans:
# 保存到数据库
add_new_plans(plans, target_month)
logger.info(f"成功为 {target_month} 生成并保存了 {len(plans)} 条月度计划。")
return True
else:
logger.warning(f"未能为 {target_month} 生成有效的月度计划。")
return False
except Exception as e:
logger.error(f"生成 {target_month} 月度计划时发生错误: {e}")
return False
finally:
self.generation_running = False
def _get_previous_month(self, current_month: str) -> str:
"""获取上个月的月份字符串"""
try:
year, month = map(int, current_month.split('-'))
if month == 1:
return f"{year-1}-12"
else:
return f"{year}-{month-1:02d}"
except Exception:
# 如果解析失败,返回一个不存在的月份
return "1900-01"
def _build_generation_prompt(self, target_month: str, archived_plans: List) -> str:
"""构建月度计划生成的 Prompt"""
# 获取配置
guidelines = getattr(global_config.monthly_plan_system, 'guidelines', None) or DEFAULT_MONTHLY_PLAN_GUIDELINES
personality = global_config.personality.personality_core
personality_side = global_config.personality.personality_side
max_plans = global_config.monthly_plan_system.max_plans_per_month
# 构建上月未完成计划的参考信息
archived_plans_block = ""
if archived_plans:
archived_texts = [f"- {plan.plan_text}" for plan in archived_plans[:5]] # 最多显示5个
archived_plans_block = f"""
**上个月未完成的一些计划(可作为参考)**:
{chr(10).join(archived_texts)}
你可以考虑是否要在这个月继续推进这些计划,或者制定全新的计划。
"""
prompt = f"""
我,{global_config.bot.nickname},需要为自己制定 {target_month} 的月度计划。
**关于我**:
- **核心人设**: {personality}
- **具体习惯与兴趣**:
{personality_side}
{archived_plans_block}
**我的月度计划制定原则**:
{guidelines}
**重要要求**:
1. 请为我生成 {max_plans} 条左右的月度计划
2. 每条计划都应该是一句话,简洁明了,具体可行
3. 计划应该涵盖不同的生活方面(学习、娱乐、社交、个人成长等)
4. 返回格式必须是纯文本,每行一条计划,不要使用 JSON 或其他格式
5. 不要包含任何解释性文字,只返回计划列表
**示例格式**:
学习一门新的编程语言或技术
每周至少看两部有趣的电影
与朋友们组织一次户外活动
阅读3本感兴趣的书籍
尝试制作一道新的料理
请你扮演我,以我的身份和兴趣,为 {target_month} 制定合适的月度计划。
"""
return prompt
async def _generate_plans_with_llm(self, prompt: str) -> List[str]:
"""使用 LLM 生成月度计划列表"""
max_retries = 3
for attempt in range(1, max_retries + 1):
try:
logger.info(f"正在生成月度计划 (第 {attempt} 次尝试)")
response, _ = await self.llm.generate_response_async(prompt)
# 解析响应
plans = self._parse_plans_response(response)
if plans:
logger.info(f"成功生成 {len(plans)} 条月度计划")
return plans
else:
logger.warning(f"{attempt} 次生成的计划为空,继续重试...")
except Exception as e:
logger.error(f"{attempt} 次生成月度计划失败: {e}")
# 添加短暂延迟,避免过于频繁的请求
if attempt < max_retries:
await asyncio.sleep(2)
logger.error("所有尝试都失败,无法生成月度计划")
return []
def _parse_plans_response(self, response: str) -> List[str]:
"""解析 LLM 响应,提取计划列表"""
try:
# 清理响应文本
response = response.strip()
# 按行分割
lines = [line.strip() for line in response.split('\n') if line.strip()]
# 过滤掉明显不是计划的行(比如包含特殊标记的行)
plans = []
for line in lines:
# 跳过包含特殊标记的行
if any(marker in line for marker in ['**', '##', '```', '---', '===', '###']):
continue
# 移除可能的序号前缀
line = line.lstrip('0123456789.- ')
# 确保计划不为空且有意义
if len(line) > 5 and not line.startswith(('', '以上', '总结', '注意')):
plans.append(line)
# 限制计划数量
max_plans = global_config.monthly_plan_system.max_plans_per_month
if len(plans) > max_plans:
plans = plans[:max_plans]
return plans
except Exception as e:
logger.error(f"解析月度计划响应时发生错误: {e}")
return []
async def archive_current_month_plans(self, target_month: Optional[str] = None):
"""
归档当前月份的活跃计划
:param target_month: 目标月份,格式为 "YYYY-MM"。如果为 None则为当前月份。
"""
try:
if target_month is None:
target_month = datetime.now().strftime("%Y-%m")
logger.info(f"开始归档 {target_month} 的活跃月度计划...")
archived_count = archive_active_plans_for_month(target_month)
logger.info(f"成功归档了 {archived_count}{target_month} 的月度计划。")
except Exception as e:
logger.error(f"归档 {target_month} 月度计划时发生错误: {e}")
class MonthlyPlanGenerationTask(AsyncTask):
"""每月初自动生成新月度计划的任务"""
def __init__(self, monthly_plan_manager: MonthlyPlanManager):
super().__init__(task_name="MonthlyPlanGenerationTask")
self.monthly_plan_manager = monthly_plan_manager
async def run(self):
while True:
try:
# 计算到下个月1号凌晨的时间
now = datetime.now()
# 获取下个月的第一天
if now.month == 12:
next_month = datetime(now.year + 1, 1, 1)
else:
next_month = datetime(now.year, now.month + 1, 1)
sleep_seconds = (next_month - now).total_seconds()
logger.info(f"下一次月度计划生成任务将在 {sleep_seconds:.2f} 秒后运行 (北京时间 {next_month.strftime('%Y-%m-%d %H:%M:%S')})")
# 等待直到下个月1号
await asyncio.sleep(sleep_seconds)
# 先归档上个月的计划
last_month = (next_month - timedelta(days=1)).strftime("%Y-%m")
await self.monthly_plan_manager.archive_current_month_plans(last_month)
# 生成新月份的计划
current_month = next_month.strftime("%Y-%m")
logger.info(f"到达月初,开始生成 {current_month} 的月度计划...")
await self.monthly_plan_manager.generate_monthly_plans(current_month)
except asyncio.CancelledError:
logger.info("每月月度计划生成任务被取消。")
break
except Exception as e:
logger.error(f"每月月度计划生成任务发生未知错误: {e}")
# 发生错误后等待1小时再重试避免频繁失败
await asyncio.sleep(3600)
# 全局实例
monthly_plan_manager = MonthlyPlanManager()

View File

@@ -7,7 +7,11 @@ from lunar_python import Lunar
from pydantic import BaseModel, ValidationError, validator from pydantic import BaseModel, ValidationError, validator
from src.common.database.sqlalchemy_models import Schedule, get_db_session from src.common.database.sqlalchemy_models import Schedule, get_db_session
from src.common.database.monthly_plan_db import get_active_plans_for_month, soft_delete_plans from src.common.database.monthly_plan_db import (
get_smart_plans_for_daily_schedule,
update_plan_usage,
soft_delete_plans # 保留兼容性
)
from src.config.config import global_config, model_config from src.config.config import global_config, model_config
from src.llm_models.utils_model import LLMRequest from src.llm_models.utils_model import LLMRequest
from src.common.logger import get_logger from src.common.logger import get_logger
@@ -211,14 +215,35 @@ class ScheduleManager:
monthly_plans_block = "" monthly_plans_block = ""
used_plan_ids = [] used_plan_ids = []
if global_config.monthly_plan_system and global_config.monthly_plan_system.enable: if global_config.monthly_plan_system and global_config.monthly_plan_system.enable:
active_plans = get_active_plans_for_month(current_month_str) # 使用新的智能抽取逻辑
if active_plans: avoid_days = getattr(global_config.monthly_plan_system, 'avoid_repetition_days', 7)
# 随机抽取最多3个计划 # 使用新的智能抽取逻辑
num_to_sample = min(len(active_plans), 3) avoid_days = getattr(global_config.monthly_plan_system, 'avoid_repetition_days', 7)
sampled_plans = random.sample(active_plans, num_to_sample) sampled_plans = get_smart_plans_for_daily_schedule(
used_plan_ids = [p.id for p in sampled_plans] # type: ignore current_month_str,
max_count=3,
avoid_days=avoid_days
)
# 如果计划耗尽,则触发补充生成
if not sampled_plans:
logger.info("可用的月度计划已耗尽或不足,尝试进行补充生成...")
from src.manager.monthly_plan_manager import monthly_plan_manager
success = await monthly_plan_manager.generate_monthly_plans(current_month_str)
if success:
logger.info("补充生成完成,重新抽取月度计划...")
sampled_plans = get_smart_plans_for_daily_schedule(
current_month_str,
max_count=3,
avoid_days=avoid_days
)
else:
logger.warning("月度计划补充生成失败。")
if sampled_plans:
used_plan_ids = [plan.id for plan in sampled_plans] # SQLAlchemy 对象的 id 属性会自动返回实际值
plan_texts = "\n".join([f"- {p.plan_text}" for p in sampled_plans]) plan_texts = "\n".join([f"- {plan.plan_text}" for plan in sampled_plans])
monthly_plans_block = f""" monthly_plans_block = f"""
**我这个月的一些小目标/计划 (请在今天的日程中适当体现)**: **我这个月的一些小目标/计划 (请在今天的日程中适当体现)**:
{plan_texts} {plan_texts}
@@ -313,11 +338,10 @@ class ScheduleManager:
self.today_schedule = schedule_data self.today_schedule = schedule_data
# 成功生成日程后,根据概率软删除使用过的月度计划 # 成功生成日程后,更新使用过的月度计划的统计信息
if used_plan_ids and global_config.monthly_plan_system: if used_plan_ids and global_config.monthly_plan_system:
if random.random() < global_config.monthly_plan_system.deletion_probability_on_use: logger.info(f"更新使用过的月度计划 {used_plan_ids} 的统计信息。")
logger.info(f"根据概率,将使用过的月度计划 {used_plan_ids} 标记为已完成。") update_plan_usage(used_plan_ids, today_str) # type: ignore
soft_delete_plans(used_plan_ids)
# 成功生成,退出无限循环 # 成功生成,退出无限循环
break break

View File

@@ -1,5 +1,5 @@
[inner] [inner]
version = "6.4.8" version = "6.4.9"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
#如果你想要修改配置文件请递增version的值 #如果你想要修改配置文件请递增version的值
@@ -435,19 +435,28 @@ search_strategy = "single" # 搜索策略: "single"(使用第一个可用引擎)
centralized_config = true # 是否启用插件配置集中化管理 centralized_config = true # 是否启用插件配置集中化管理
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 月计划系统设置 (Monthly Plan System Settings) # 月计划系统设置 (Monthly Plan System Settings)
# ---------------------------------------------------------------- # ----------------------------------------------------------------
[monthly_plan_system] [monthly_plan_system]
# 是否启用本功能 # 是否启用本功能
enable = true enable = true
# 启动时如果当月计划少于此数量则触发LLM生成
generation_threshold = 20 # 每个月允许存在的最大计划数量
# 每次调用LLM期望生成的计划数量 max_plans_per_month = 20
plans_per_generation = 4
# 计划使用后,被删除的概率 (0.0 到 1.0) # 计划使用多少次后自动标记为已完成
deletion_probability_on_use = 0.5 completion_threshold = 3
#每个月允许存在的最大计划数量
max_plans_per_month = 30 # 多少天内不重复抽取同一个计划
avoid_repetition_days = 7
# 月度计划生成的指导原则(可选,如果不设置则使用默认原则)
guidelines = """
我希望你能为自己制定一些有意义的月度小目标和计划。
这些计划应该涵盖学习、娱乐、社交、个人成长等各个方面。
每个计划都应该是具体可行的,能够在一个月内通过日常活动逐步实现。
请确保计划既有挑战性又不会过于繁重,保持生活的平衡和乐趣。
"""
[wakeup_system] [wakeup_system]
enable = true #"是否启用唤醒度系统" enable = true #"是否启用唤醒度系统"