Merge branch 'master' into windpicker-adapter

This commit is contained in:
Windpicker-owo
2025-08-27 01:59:59 +08:00
17 changed files with 702 additions and 102 deletions

View File

@@ -4,7 +4,6 @@
if __name__ == "__main__": if __name__ == "__main__":
# 设置Python路径并执行bot.py # 设置Python路径并执行bot.py
import sys import sys
import os
from pathlib import Path from pathlib import Path
# 添加当前目录到Python路径 # 添加当前目录到Python路径

View File

@@ -11,7 +11,7 @@ from src.plugin_system.base.base_plugin import BasePlugin
from src.plugin_system.base.base_command import BaseCommand from src.plugin_system.base.base_command import BaseCommand
from src.plugin_system.apis.logging_api import get_logger from src.plugin_system.apis.logging_api import get_logger
from src.plugin_system.base.config_types import ConfigField from src.plugin_system.base.config_types import ConfigField
from src.plugin_system.utils.permission_decorators import require_permission, require_master, PermissionChecker from src.plugin_system.utils.permission_decorators import require_permission, require_master
from src.common.message import ChatStream, Message from src.common.message import ChatStream, Message

View File

@@ -2,7 +2,6 @@
import time import time
import re import re
import orjson import orjson
import ast
import traceback import traceback
from json_repair import repair_json from json_repair import repair_json

View File

@@ -392,7 +392,7 @@ class DefaultReplyer:
if not from_plugin: if not from_plugin:
result = await event_manager.trigger_event(EventType.AFTER_LLM,prompt=prompt,llm_response=llm_response,stream_id=stream_id) result = await event_manager.trigger_event(EventType.AFTER_LLM,prompt=prompt,llm_response=llm_response,stream_id=stream_id)
if not result.all_continue_process(): if not result.all_continue_process():
raise UserWarning(f"插件{result.get_summary().get("stopped_handlers","")}于请求后取消了内容生成") raise UserWarning(f"插件{result.get_summary().get('stopped_handlers','')}于请求后取消了内容生成")
except UserWarning as e: except UserWarning as e:
raise e raise e
except Exception as llm_e: except Exception as llm_e:

View File

@@ -18,8 +18,6 @@ from pathlib import Path
from typing import List, Tuple, Optional, Dict from typing import List, Tuple, Optional, Dict
import io import io
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from functools import partial
import numpy as np
from src.llm_models.utils_model import LLMRequest from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config, model_config from src.config.config import global_config, model_config

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

@@ -47,7 +47,8 @@ from src.config.official_configs import (
WakeUpSystemConfig, WakeUpSystemConfig,
MonthlyPlanSystemConfig, MonthlyPlanSystemConfig,
CrossContextConfig, CrossContextConfig,
PermissionConfig PermissionConfig,
MaizoneIntercomConfig,
) )
from .api_ada_configs import ( from .api_ada_configs import (
@@ -396,6 +397,7 @@ class Config(ValidatedConfigBase):
wakeup_system: WakeUpSystemConfig = Field(default_factory=lambda: WakeUpSystemConfig(), description="唤醒度系统配置") wakeup_system: WakeUpSystemConfig = Field(default_factory=lambda: WakeUpSystemConfig(), description="唤醒度系统配置")
monthly_plan_system: MonthlyPlanSystemConfig = Field(default_factory=lambda: MonthlyPlanSystemConfig(), description="月层计划系统配置") monthly_plan_system: MonthlyPlanSystemConfig = Field(default_factory=lambda: MonthlyPlanSystemConfig(), description="月层计划系统配置")
cross_context: CrossContextConfig = Field(default_factory=lambda: CrossContextConfig(), description="跨群聊上下文共享配置") cross_context: CrossContextConfig = Field(default_factory=lambda: CrossContextConfig(), description="跨群聊上下文共享配置")
maizone_intercom: MaizoneIntercomConfig = Field(default_factory=lambda: MaizoneIntercomConfig(), description="Maizone互通组配置")
class APIAdapterConfig(ValidatedConfigBase): class APIAdapterConfig(ValidatedConfigBase):

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):
@@ -701,6 +701,12 @@ class CrossContextConfig(ValidatedConfigBase):
groups: List[ContextGroup] = Field(default_factory=list, description="上下文共享组列表") groups: List[ContextGroup] = Field(default_factory=list, description="上下文共享组列表")
class MaizoneIntercomConfig(ValidatedConfigBase):
"""Maizone互通组配置"""
enable: bool = Field(default=False, description="是否启用Maizone互通组功能")
groups: List[ContextGroup] = Field(default_factory=list, description="Maizone互通组列表")
class PermissionConfig(ValidatedConfigBase): class PermissionConfig(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,313 @@
# mmc/src/manager/monthly_plan_manager.py
import asyncio
from datetime import datetime, timedelta
from typing import List, Optional
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

@@ -1,13 +1,15 @@
import orjson import orjson
import asyncio import asyncio
import random
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
from lunar_python import Lunar 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 # 保留兼容性
)
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 +213,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
)
plan_texts = "\n".join([f"- {p.plan_text}" for p in sampled_plans]) # 如果计划耗尽,则触发补充生成
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"- {plan.plan_text}" for plan in sampled_plans])
monthly_plans_block = f""" monthly_plans_block = f"""
**我这个月的一些小目标/计划 (请在今天的日程中适当体现)**: **我这个月的一些小目标/计划 (请在今天的日程中适当体现)**:
{plan_texts} {plan_texts}
@@ -313,11 +336,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

@@ -20,6 +20,7 @@ from src.plugin_system.apis import (
tool_api, tool_api,
permission_api, permission_api,
) )
from src.plugin_system.apis.chat_api import ChatManager as context_api
from .logging_api import get_logger from .logging_api import get_logger
from .plugin_register_api import register_plugin from .plugin_register_api import register_plugin
@@ -40,4 +41,5 @@ __all__ = [
"register_plugin", "register_plugin",
"tool_api", "tool_api",
"permission_api", "permission_api",
"context_api",
] ]

View File

@@ -10,14 +10,12 @@ from src.plugin_system.base.component_types import (
CommandInfo, CommandInfo,
EventHandlerInfo, EventHandlerInfo,
PluginInfo, PluginInfo,
EventInfo,
ComponentType, ComponentType,
) )
from src.plugin_system.base.base_command import BaseCommand from src.plugin_system.base.base_command import BaseCommand
from src.plugin_system.base.base_action import BaseAction from src.plugin_system.base.base_action import BaseAction
from src.plugin_system.base.base_tool import BaseTool from src.plugin_system.base.base_tool import BaseTool
from src.plugin_system.base.base_events_handler import BaseEventHandler from src.plugin_system.base.base_events_handler import BaseEventHandler
from src.plugin_system.base.base_event import BaseEvent
logger = get_logger("component_registry") logger = get_logger("component_registry")

View File

@@ -3,7 +3,7 @@
内容服务模块 内容服务模块
负责生成所有与QQ空间相关的文本内容例如说说、评论等。 负责生成所有与QQ空间相关的文本内容例如说说、评论等。
""" """
from typing import Callable from typing import Callable, Optional
import datetime import datetime
from src.common.logger import get_logger from src.common.logger import get_logger
@@ -28,11 +28,12 @@ class ContentService:
""" """
self.get_config = get_config self.get_config = get_config
async def generate_story(self, topic: str) -> str: async def generate_story(self, topic: str, context: Optional[str] = None) -> str:
""" """
根据指定主题生成一条QQ空间说说。 根据指定主题和可选的上下文生成一条QQ空间说说。
:param topic: 说说的主题。 :param topic: 说说的主题。
:param context: 可选的聊天上下文。
:return: 生成的说说内容,如果失败则返回空字符串。 :return: 生成的说说内容,如果失败则返回空字符串。
""" """
try: try:
@@ -57,23 +58,19 @@ class ContentService:
weekday = weekday_names[now.weekday()] weekday = weekday_names[now.weekday()]
# 构建提示词 # 构建提示词
if topic: prompt_topic = f"主题是'{topic}'" if topic else "主题不限"
prompt = f""" prompt = f"""
你是'{bot_personality}',现在是{current_time}{weekday}),你想写一条主题是'{topic}'的说说发表在qq空间上 你是'{bot_personality}',现在是{current_time}{weekday}),你想写一条{prompt_topic}的说说发表在qq空间上
{bot_expression} {bot_expression}
不要刻意突出自身学科背景,不要浮夸,不要夸张修辞,可以适当使用颜文字 不要刻意突出自身学科背景,不要浮夸,不要夸张修辞,可以适当使用颜文字
你可以在说说中自然地提及当前的时间(如"今天""现在""此刻"等),让说说更贴近发布时间 你可以在说说中自然地提及当前的时间(如"今天""现在""此刻"等),让说说更贴近发布时间
只输出一条说说正文的内容,不要有其他的任何正文以外的冗余输出 只输出一条说说正文的内容,不要有其他的任何正文以外的冗余输出
"""
else:
prompt = f"""
你是'{bot_personality}',现在是{current_time}{weekday}你想写一条说说发表在qq空间上主题不限
{bot_expression}
不要刻意突出自身学科背景,不要浮夸,不要夸张修辞,可以适当使用颜文字,
你可以在说说中自然地提及当前的时间(如"今天""现在""此刻"等),让说说更贴近发布时间,
只输出一条说说正文的内容,不要有其他的任何正文以外的冗余输出
""" """
# 如果有上下文则加入到prompt中
if context:
prompt += f"\n作为参考,这里有一些最近的聊天记录:\n---\n{context}\n---"
# 添加历史记录以避免重复 # 添加历史记录以避免重复
prompt += "\n以下是你以前发过的说说,写新说说时注意不要在相隔不长的时间发送相同主题的说说" prompt += "\n以下是你以前发过的说说,写新说说时注意不要在相隔不长的时间发送相同主题的说说"
history_block = await get_send_history(qq_account) history_block = await get_send_history(qq_account)

View File

@@ -17,7 +17,12 @@ import aiohttp
import bs4 import bs4
import json5 import json5
from src.common.logger import get_logger from src.common.logger import get_logger
from src.plugin_system.apis import config_api, person_api from src.plugin_system.apis import config_api, person_api,chat_api
from src.chat.message_receive.chat_stream import get_chat_manager
from src.chat.utils.chat_message_builder import (
build_readable_messages_with_id,
get_raw_msg_by_timestamp_with_chat,
)
from .content_service import ContentService from .content_service import ContentService
from .image_service import ImageService from .image_service import ImageService
@@ -55,7 +60,10 @@ class QZoneService:
async def send_feed(self, topic: str, stream_id: Optional[str]) -> Dict[str, Any]: async def send_feed(self, topic: str, stream_id: Optional[str]) -> Dict[str, Any]:
"""发送一条说说""" """发送一条说说"""
story = await self.content_service.generate_story(topic) # --- 获取互通组上下文 ---
context = await self._get_intercom_context(stream_id) if stream_id else None
story = await self.content_service.generate_story(topic, context=context)
if not story: if not story:
return {"success": False, "message": "生成说说内容失败"} return {"success": False, "message": "生成说说内容失败"}
@@ -167,6 +175,65 @@ class QZoneService:
# --- Internal Helper Methods --- # --- Internal Helper Methods ---
async def _get_intercom_context(self, stream_id: str) -> Optional[str]:
"""
根据 stream_id 查找其所属的互通组,并构建该组的聊天上下文。
Args:
stream_id: 需要查找的当前聊天流ID。
Returns:
如果找到匹配的组,则返回一个包含聊天记录的字符串;否则返回 None。
"""
intercom_config = config_api.get_global_config("maizone_intercom")
if not (intercom_config and intercom_config.enable):
return None
chat_manager = get_chat_manager()
bot_platform = config_api.get_global_config('bot.platform')
for group in intercom_config.groups:
# 使用集合以优化查找效率
group_stream_ids = {
chat_manager.get_stream_id(bot_platform, chat_id, True)
for chat_id in group.chat_ids
}
if stream_id in group_stream_ids:
logger.debug(f"Stream ID '{stream_id}' 在互通组 '{getattr(group, 'name', 'Unknown')}' 中找到,正在构建上下文。")
all_messages = []
end_time = time.time()
start_time = end_time - (3 * 24 * 60 * 60) # 获取过去3天的消息
for chat_id in group.chat_ids:
# 使用正确的函数获取历史消息
messages = get_raw_msg_by_timestamp_with_chat(
chat_id=chat_id,
timestamp_start=start_time,
timestamp_end=end_time,
limit=20, # 每个聊天最多获取20条
limit_mode="latest"
)
all_messages.extend(messages)
if not all_messages:
return None
# 按时间戳对所有消息进行排序
all_messages.sort(key=lambda x: x.get("time", 0))
# 限制总消息数例如最多100条
if len(all_messages) > 100:
all_messages = all_messages[-100:]
# build_readable_messages_with_id 返回一个元组 (formatted_string, message_id_list)
formatted_string, _ = build_readable_messages_with_id(all_messages)
return formatted_string
logger.debug(f"Stream ID '{stream_id}' 未在任何互通组中找到。")
return None
async def _reply_to_own_feed_comments(self, feed: Dict, api_client: Dict): async def _reply_to_own_feed_comments(self, feed: Dict, api_client: Dict):
"""处理对自己说说的评论并进行回复""" """处理对自己说说的评论并进行回复"""
qq_account = config_api.get_global_config("bot.qq_account", "") qq_account = config_api.get_global_config("bot.qq_account", "")
@@ -290,15 +357,15 @@ class QZoneService:
if cookie_data and "cookies" in cookie_data: if cookie_data and "cookies" in cookie_data:
cookie_str = cookie_data["cookies"] cookie_str = cookie_data["cookies"]
parsed_cookies = {k.strip(): v.strip() for k, v in (p.split('=', 1) for p in cookie_str.split('; ') if '=' in p)} parsed_cookies = {k.strip(): v.strip() for k, v in (p.split('=', 1) for p in cookie_str.split('; ') if '=' in p)}
with open(cookie_file_path, "w", encoding="utf-8") as f: with open(cookie_file_path, "wb") as f:
orjson.dump(parsed_cookies, f) f.write(orjson.dumps(parsed_cookies))
logger.info(f"Cookie已更新并保存至: {cookie_file_path}") logger.info(f"Cookie已更新并保存至: {cookie_file_path}")
return parsed_cookies return parsed_cookies
# 如果HTTP获取失败尝试读取本地文件 # 如果HTTP获取失败尝试读取本地文件
if cookie_file_path.exists(): if cookie_file_path.exists():
with open(cookie_file_path, "r", encoding="utf-8") as f: with open(cookie_file_path, "rb") as f:
return orjson.loads(f) return orjson.loads(f.read())
return None return None
except Exception as e: except Exception as e:
logger.error(f"更新或加载Cookie时发生异常: {e}") logger.error(f"更新或加载Cookie时发生异常: {e}")
@@ -682,25 +749,23 @@ class QZoneService:
# 处理不同的响应格式 # 处理不同的响应格式
json_str = "" json_str = ""
# 使用strip()处理可能存在的前后空白字符
stripped_res_text = res_text.strip() stripped_res_text = res_text.strip()
if stripped_res_text.startswith('_Callback(') and stripped_res_text.endswith(');'): if stripped_res_text.startswith('_Callback(') and stripped_res_text.endswith(');'):
# JSONP格式
json_str = stripped_res_text[len('_Callback('):-2] json_str = stripped_res_text[len('_Callback('):-2]
elif stripped_res_text.startswith('{') and stripped_res_text.endswith('}'): elif stripped_res_text.startswith('{') and stripped_res_text.endswith('}'):
# 直接JSON格式
json_str = stripped_res_text json_str = stripped_res_text
else: else:
logger.warning(f"意外的响应格式: {res_text[:100]}...") logger.warning(f"意外的响应格式: {res_text[:100]}...")
return [] return []
# 清理和标准化JSON字符串
json_str = json_str.replace('undefined', 'null').strip() json_str = json_str.replace('undefined', 'null').strip()
try: try:
json_data = json5.loads(json_str) json_data = json5.loads(json_str)
if not isinstance(json_data, dict):
logger.warning(f"解析后的JSON数据不是字典类型: {type(json_data)}")
return []
# 检查API返回的错误码
if json_data.get('code') != 0: if json_data.get('code') != 0:
error_code = json_data.get('code') error_code = json_data.get('code')
error_msg = json_data.get('message', '未知错误') error_msg = json_data.get('message', '未知错误')
@@ -710,6 +775,7 @@ class QZoneService:
except Exception as parse_error: except Exception as parse_error:
logger.error(f"JSON解析失败: {parse_error}, 原始数据: {json_str[:200]}...") logger.error(f"JSON解析失败: {parse_error}, 原始数据: {json_str[:200]}...")
return [] return []
feeds_data = [] feeds_data = []
if isinstance(json_data, dict): if isinstance(json_data, dict):
data_level1 = json_data.get('data') data_level1 = json_data.get('data')
@@ -718,10 +784,9 @@ class QZoneService:
feeds_list = [] feeds_list = []
for feed in feeds_data: for feed in feeds_data:
if not feed: if not feed or not isinstance(feed, dict):
continue continue
# 过滤非说说动态
if str(feed.get('appid', '')) != '311': if str(feed.get('appid', '')) != '311':
continue continue
@@ -730,7 +795,6 @@ class QZoneService:
if not target_qq or not tid: if not target_qq or not tid:
continue continue
# 跳过自己的说说(监控是看好友的)
if target_qq == str(uin): if target_qq == str(uin):
continue continue
@@ -740,16 +804,14 @@ class QZoneService:
soup = bs4.BeautifulSoup(html_content, 'html.parser') soup = bs4.BeautifulSoup(html_content, 'html.parser')
# 通过点赞状态判断是否已读/处理过
like_btn = soup.find('a', class_='qz_like_btn_v3') like_btn = soup.find('a', class_='qz_like_btn_v3')
is_liked = False is_liked = False
if like_btn: if like_btn and isinstance(like_btn, bs4.Tag):
is_liked = like_btn.get('data-islike') == '1' is_liked = like_btn.get('data-islike') == '1'
if is_liked: if is_liked:
continue # 如果已经点赞过,说明是已处理的说说,跳过 continue
# 提取内容
text_div = soup.find('div', class_='f-info') text_div = soup.find('div', class_='f-info')
text = text_div.get_text(strip=True) if text_div else "" text = text_div.get_text(strip=True) if text_div else ""

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期望生成的计划数量
plans_per_generation = 4
# 计划被使用后,被删除的概率 (0.0 到 1.0)
deletion_probability_on_use = 0.5
# 每个月允许存在的最大计划数量 # 每个月允许存在的最大计划数量
max_plans_per_month = 30 max_plans_per_month = 20
# 计划使用多少次后自动标记为已完成
completion_threshold = 3
# 多少天内不重复抽取同一个计划
avoid_repetition_days = 7
# 月度计划生成的指导原则(可选,如果不设置则使用默认原则)
guidelines = """
我希望你能为自己制定一些有意义的月度小目标和计划。
这些计划应该涵盖学习、娱乐、社交、个人成长等各个方面。
每个计划都应该是具体可行的,能够在一个月内通过日常活动逐步实现。
请确保计划既有挑战性又不会过于繁重,保持生活的平衡和乐趣。
"""
[wakeup_system] [wakeup_system]
enable = true #"是否启用唤醒度系统" enable = true #"是否启用唤醒度系统"
@@ -473,9 +482,16 @@ chat_ids = [
"222222" # 假设这是“产品群”的ID "222222" # 假设这是“产品群”的ID
] ]
[[cross_context.groups]] [maizone_intercom]
name = "日常闲聊组" # QQ空间互通组配置
# 启用后,发布说说时会读取指定互通组的上下文
enable = false
# 定义QQ空间互通组
# 同一个组的chat_id会共享上下文用于生成更相关的说说
[[maizone_intercom.groups]]
name = "Maizone默认互通组"
chat_ids = [ chat_ids = [
"333333", # 假设这是“吹水群”的ID "111111", # 示例群聊1
"444444" # 假设这是“游戏群”的ID "222222" # 示例群聊2
] ]