Merge pull request #947 from Oct-autumn/dev

refactor: 阶段性重构PR
This commit is contained in:
SengokuCola
2025-05-13 12:14:43 +08:00
committed by GitHub
20 changed files with 1429 additions and 941 deletions

3
.gitignore vendored
View File

@@ -35,7 +35,6 @@ config/bot_config.toml
config/bot_config.toml.bak config/bot_config.toml.bak
config/lpmm_config.toml config/lpmm_config.toml
config/lpmm_config.toml.bak config/lpmm_config.toml.bak
src/plugins/remote/client_uuid.json
(测试版)麦麦生成人格.bat (测试版)麦麦生成人格.bat
(临时版)麦麦开始学习.bat (临时版)麦麦开始学习.bat
src/plugins/utils/statistic.py src/plugins/utils/statistic.py
@@ -43,7 +42,7 @@ src/plugins/utils/statistic.py
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
llm_statistics.txt maibot_statistics.html
mongodb mongodb
napcat napcat
run_dev.bat run_dev.bat

43
bot.py
View File

@@ -1,7 +1,6 @@
import asyncio import asyncio
import hashlib import hashlib
import os import os
import shutil
import sys import sys
from pathlib import Path from pathlib import Path
import time import time
@@ -15,6 +14,8 @@ from src.common.crash_logger import install_crash_handler
from src.main import MainSystem from src.main import MainSystem
from rich.traceback import install from rich.traceback import install
from src.manager.async_task_manager import async_task_manager
install(extra_lines=3) install(extra_lines=3)
# 设置工作目录为脚本所在目录 # 设置工作目录为脚本所在目录
@@ -64,38 +65,6 @@ def easter_egg():
print(rainbow_text) print(rainbow_text)
def init_config():
# 初次启动检测
if not os.path.exists("config/bot_config.toml"):
logger.warning("检测到bot_config.toml不存在正在从模板复制")
# 检查config目录是否存在
if not os.path.exists("config"):
os.makedirs("config")
logger.info("创建config目录")
shutil.copy("template/bot_config_template.toml", "config/bot_config.toml")
logger.info("复制完成请修改config/bot_config.toml和.env中的配置后重新启动")
if not os.path.exists("config/lpmm_config.toml"):
logger.warning("检测到lpmm_config.toml不存在正在从模板复制")
# 检查config目录是否存在
if not os.path.exists("config"):
os.makedirs("config")
logger.info("创建config目录")
shutil.copy("template/lpmm_config_template.toml", "config/lpmm_config.toml")
logger.info("复制完成请修改config/lpmm_config.toml和.env中的配置后重新启动")
def init_env():
# 检测.env文件是否存在
if not os.path.exists(".env"):
logger.error("检测到.env文件不存在")
shutil.copy("template/template.env", "./.env")
logger.info("已从template/template.env复制创建.env请修改配置后重新启动")
def load_env(): def load_env():
# 直接加载生产环境变量配置 # 直接加载生产环境变量配置
if os.path.exists(".env"): if os.path.exists(".env"):
@@ -140,6 +109,10 @@ def scan_provider(env_config: dict):
async def graceful_shutdown(): async def graceful_shutdown():
try: try:
logger.info("正在优雅关闭麦麦...") logger.info("正在优雅关闭麦麦...")
# 停止所有异步任务
await async_task_manager.stop_and_wait_all_tasks()
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
for task in tasks: for task in tasks:
task.cancel() task.cancel()
@@ -235,9 +208,9 @@ def raw_main():
check_eula() check_eula()
print("检查EULA和隐私条款完成") print("检查EULA和隐私条款完成")
easter_egg() easter_egg()
init_config()
init_env()
load_env() load_env()
env_config = {key: os.getenv(key) for key in os.environ} env_config = {key: os.getenv(key) for key in os.environ}

View File

@@ -1,10 +1,10 @@
from src.do_tool.tool_can_use.base_tool import BaseTool
from src.config.config import global_config
from src.common.logger_manager import get_logger
from src.plugins.moods.moods import MoodManager
from typing import Any from typing import Any
from src.common.logger_manager import get_logger
from src.config.config import global_config
from src.do_tool.tool_can_use.base_tool import BaseTool
from src.manager.mood_manager import mood_manager
logger = get_logger("change_mood_tool") logger = get_logger("change_mood_tool")
@@ -36,7 +36,6 @@ class ChangeMoodTool(BaseTool):
response_set = function_args.get("response_set") response_set = function_args.get("response_set")
_message_processed_plain_text = function_args.get("text") _message_processed_plain_text = function_args.get("text")
mood_manager = MoodManager.get_instance()
# gpt = ResponseGenerator() # gpt = ResponseGenerator()
if response_set is None: if response_set is None:

View File

@@ -1,4 +1,4 @@
from src.plugins.moods.moods import MoodManager from src.manager.mood_manager import mood_manager
import enum import enum
@@ -13,5 +13,5 @@ class ChatStateInfo:
self.chat_status: ChatState = ChatState.ABSENT self.chat_status: ChatState = ChatState.ABSENT
self.current_state_time = 120 self.current_state_time = 120
self.mood_manager = MoodManager() self.mood_manager = mood_manager
self.mood = self.mood_manager.get_prompt() self.mood = self.mood_manager.get_mood_prompt()

View File

@@ -3,7 +3,7 @@ import time
import random import random
from typing import List, Tuple, Optional from typing import List, Tuple, Optional
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
from src.plugins.moods.moods import MoodManager from src.manager.mood_manager import mood_manager
from src.config.config import global_config from src.config.config import global_config
logger = get_logger("mai_state") logger = get_logger("mai_state")
@@ -88,7 +88,7 @@ class MaiStateInfo:
self.last_min_check_time: float = time.time() # 上次1分钟规则检查时间 self.last_min_check_time: float = time.time() # 上次1分钟规则检查时间
# Mood management is now part of MaiStateInfo # Mood management is now part of MaiStateInfo
self.mood_manager = MoodManager.get_instance() # Use singleton instance self.mood_manager = mood_manager # Use singleton instance
def update_mai_status(self, new_status: MaiState) -> bool: def update_mai_status(self, new_status: MaiState) -> bool:
""" """
@@ -124,7 +124,7 @@ class MaiStateInfo:
def get_mood_prompt(self) -> str: def get_mood_prompt(self) -> str:
"""获取当前的心情提示词""" """获取当前的心情提示词"""
# Delegate to the internal mood manager # Delegate to the internal mood manager
return self.mood_manager.get_prompt() return self.mood_manager.get_mood_prompt()
def get_current_state(self) -> MaiState: def get_current_state(self) -> MaiState:
"""获取当前的 MaiState""" """获取当前的 MaiState"""

View File

@@ -1,7 +1,12 @@
import asyncio import asyncio
import time import time
from .plugins.utils.statistic import LLMStatistics
from .plugins.moods.moods import MoodManager from maim_message import MessageServer
from .plugins.remote.remote import TelemetryHeartBeatTask
from .manager.async_task_manager import async_task_manager
from .plugins.utils.statistic import OnlineTimeRecordTask, StatisticOutputTask
from .manager.mood_manager import MoodPrintTask, MoodUpdateTask
from .plugins.schedule.schedule_generator import bot_schedule from .plugins.schedule.schedule_generator import bot_schedule
from .plugins.emoji_system.emoji_manager import emoji_manager from .plugins.emoji_system.emoji_manager import emoji_manager
from .plugins.person_info.person_info import person_info_manager from .plugins.person_info.person_info import person_info_manager
@@ -14,9 +19,8 @@ from .plugins.storage.storage import MessageStorage
from .config.config import global_config from .config.config import global_config
from .plugins.chat.bot import chat_bot from .plugins.chat.bot import chat_bot
from .common.logger_manager import get_logger from .common.logger_manager import get_logger
from .plugins.remote import heartbeat_thread # noqa: F401
from .individuality.individuality import Individuality from .individuality.individuality import Individuality
from .common.server import global_server from .common.server import global_server, Server
from rich.traceback import install from rich.traceback import install
from .api.main import start_api_server from .api.main import start_api_server
@@ -27,17 +31,14 @@ logger = get_logger("main")
class MainSystem: class MainSystem:
def __init__(self): def __init__(self):
self.llm_stats = LLMStatistics("llm_statistics.txt") self.hippocampus_manager: HippocampusManager = HippocampusManager.get_instance()
self.mood_manager = MoodManager.get_instance() self.individuality: Individuality = Individuality.get_instance()
self.hippocampus_manager = HippocampusManager.get_instance()
self._message_manager_started = False
self.individuality = Individuality.get_instance()
# 使用消息API替代直接的FastAPI实例 # 使用消息API替代直接的FastAPI实例
from .plugins.message import global_api from .plugins.message import global_api
self.app = global_api self.app: MessageServer = global_api
self.server = global_server self.server: Server = global_server
async def initialize(self): async def initialize(self):
"""初始化系统组件""" """初始化系统组件"""
@@ -51,9 +52,15 @@ class MainSystem:
async def _init_components(self): async def _init_components(self):
"""初始化其他组件""" """初始化其他组件"""
init_start_time = time.time() init_start_time = time.time()
# 启动LLM统计
self.llm_stats.start() # 添加在线时间统计任务
logger.success("LLM统计功能启动成功") await async_task_manager.add_task(OnlineTimeRecordTask())
# 添加统计信息输出任务
await async_task_manager.add_task(StatisticOutputTask())
# 添加遥测心跳任务
await async_task_manager.add_task(TelemetryHeartBeatTask())
# 启动API服务器 # 启动API服务器
start_api_server() start_api_server()
@@ -62,9 +69,10 @@ class MainSystem:
emoji_manager.initialize() emoji_manager.initialize()
logger.success("表情包管理器初始化成功") logger.success("表情包管理器初始化成功")
# 启动情绪管理器 # 添加情绪衰减任务
self.mood_manager.start_mood_update(update_interval=global_config.mood_update_interval) await async_task_manager.add_task(MoodUpdateTask())
logger.success("情绪管理器启动成功") # 添加情绪打印任务
await async_task_manager.add_task(MoodPrintTask())
# 检查并清除person_info冗余字段启动个人习惯推断 # 检查并清除person_info冗余字段启动个人习惯推断
await person_info_manager.del_all_undefined_field() await person_info_manager.del_all_undefined_field()
@@ -129,7 +137,6 @@ class MainSystem:
self.build_memory_task(), self.build_memory_task(),
self.forget_memory_task(), self.forget_memory_task(),
self.consolidate_memory_task(), self.consolidate_memory_task(),
self.print_mood_task(),
self.remove_recalled_message_task(), self.remove_recalled_message_task(),
emoji_manager.start_periodic_check_register(), emoji_manager.start_periodic_check_register(),
self.app.run(), self.app.run(),
@@ -163,12 +170,6 @@ class MainSystem:
await HippocampusManager.get_instance().consolidate_memory() await HippocampusManager.get_instance().consolidate_memory()
print("\033[1;32m[记忆整合]\033[0m 记忆整合完成") print("\033[1;32m[记忆整合]\033[0m 记忆整合完成")
async def print_mood_task(self):
"""打印情绪状态"""
while True:
self.mood_manager.print_mood_status()
await asyncio.sleep(60)
@staticmethod @staticmethod
async def remove_recalled_message_task(): async def remove_recalled_message_task():
"""删除撤回消息任务""" """删除撤回消息任务"""

View File

@@ -0,0 +1,150 @@
from abc import abstractmethod
import asyncio
from asyncio import Task, Event, Lock
from typing import Callable, Dict
from src.common.logger_manager import get_logger
logger = get_logger("async_task_manager")
class AsyncTask:
"""异步任务基类"""
def __init__(self, task_name: str | None = None, wait_before_start: int = 0, run_interval: int = 0):
self.task_name: str = task_name or self.__class__.__name__
"""任务名称"""
self.wait_before_start: int = wait_before_start
"""运行任务前是否进行等待单位设为0则不等待"""
self.run_interval: int = run_interval
"""多次运行的时间间隔单位设为0则仅运行一次"""
@abstractmethod
async def run(self):
"""
任务的执行过程
"""
pass
async def start_task(self, abort_flag: asyncio.Event):
if self.wait_before_start > 0:
# 等待指定时间后开始任务
await asyncio.sleep(self.wait_before_start)
while not abort_flag.is_set():
await self.run()
if self.run_interval > 0:
await asyncio.sleep(self.run_interval)
else:
break
class AsyncTaskManager:
"""异步任务管理器"""
def __init__(self):
self.tasks: Dict[str, Task] = {}
"""任务列表"""
self.abort_flag: Event = Event()
"""是否中止任务标志"""
self._lock: Lock = Lock()
"""异步锁当可能出现await时需要加锁"""
def _remove_task_call_back(self, task: Task):
"""
call_back: 任务完成后移除任务
"""
task_name = task.get_name()
if task_name in self.tasks:
# 任务完成后移除任务
del self.tasks[task_name]
logger.debug(f"已移除任务 '{task_name}'")
else:
logger.warning(f"尝试移除不存在的任务 '{task_name}'")
@staticmethod
def _default_finish_call_back(task: Task):
"""
call_back: 默认的任务完成回调函数
"""
try:
task.result()
logger.debug(f"任务 '{task.get_name()}' 完成")
except asyncio.CancelledError:
logger.debug(f"任务 '{task.get_name()}' 被取消")
except Exception as e:
logger.error(f"任务 '{task.get_name()}' 执行时发生异常: {e}", exc_info=True)
async def add_task(self, task: AsyncTask, call_back: Callable[[asyncio.Task], None] | None = None):
"""
添加任务
"""
if not issubclass(task.__class__, AsyncTask):
raise TypeError(f"task '{task.__class__.__name__}' 必须是继承 AsyncTask 的子类")
async with self._lock: # 由于可能需要await等待任务完成所以需要加异步锁
if task.task_name in self.tasks:
logger.warning(f"已存在名称为 '{task.task_name}' 的任务,正在尝试取消并替换")
self.tasks[task.task_name].cancel() # 取消已存在的任务
await self.tasks[task.task_name] # 等待任务完成
logger.info(f"成功结束任务 '{task.task_name}'")
# 创建新任务
task_inst = asyncio.create_task(task.start_task(self.abort_flag))
task_inst.set_name(task.task_name)
task_inst.add_done_callback(self._remove_task_call_back) # 添加完成回调函数-完成任务后自动移除任务
task_inst.add_done_callback(
call_back or self._default_finish_call_back
) # 添加完成回调函数-用户自定义或默认的FallBack
self.tasks[task.task_name] = task_inst # 将任务添加到任务列表
logger.info(f"已启动任务 '{task.task_name}'")
def get_tasks_status(self) -> Dict[str, Dict[str, str]]:
"""
获取所有任务的状态
"""
tasks_status = {}
for task_name, task in self.tasks.items():
tasks_status[task_name] = {
"status": "running" if not task.done() else "done",
}
return tasks_status
async def stop_and_wait_all_tasks(self):
"""
终止所有任务并等待它们完成该方法会阻塞其它尝试add_task()的操作)
"""
async with self._lock: # 由于可能需要await等待任务完成所以需要加异步锁
# 设置中止标志
self.abort_flag.set()
# 取消所有任务
for name, inst in self.tasks.items():
try:
inst.cancel()
except asyncio.CancelledError:
logger.info(f"已取消任务 '{name}'")
# 等待所有任务完成
for task_name, task_inst in self.tasks.items():
if not task_inst.done():
try:
await task_inst
except asyncio.CancelledError: # 此处再次捕获取消异常防止stop_all_tasks()时延迟抛出异常
logger.info(f"任务 {task_name} 已取消")
except Exception as e:
logger.error(f"任务 {task_name} 执行时发生异常: {e}", ext_info=True)
# 清空任务列表
self.tasks.clear()
self.abort_flag.clear()
logger.info("所有异步任务已停止")
async_task_manager = AsyncTaskManager()
"""全局异步任务管理器实例"""

View File

@@ -0,0 +1,67 @@
import json
import os
from src.common.logger_manager import get_logger
LOCAL_STORE_FILE_PATH = "data/local_store.json"
logger = get_logger("local_storage")
class LocalStoreManager:
file_path: str
"""本地存储路径"""
store: dict[str, str | list | dict | int | float | bool]
"""本地存储数据"""
def __init__(self, local_store_path: str | None = None):
self.file_path = local_store_path or LOCAL_STORE_FILE_PATH
self.store = {}
self.load_local_store()
def __getitem__(self, item: str) -> str | list | dict | int | float | bool | None:
"""获取本地存储数据"""
return self.store.get(item, None)
def __setitem__(self, key: str, value: str | list | dict | int | float | bool):
"""设置本地存储数据"""
self.store[key] = value
self.save_local_store()
def __contains__(self, item: str) -> bool:
"""检查本地存储数据是否存在"""
return item in self.store
def load_local_store(self):
"""加载本地存储数据"""
if os.path.exists(self.file_path):
# 存在本地存储文件,加载数据
logger.info("正在阅读记事本......我在看,我真的在看!")
logger.debug(f"加载本地存储数据: {self.file_path}")
try:
with open(self.file_path, "r", encoding="utf-8") as f:
self.store = json.load(f)
logger.success("全都记起来了!")
except json.JSONDecodeError:
logger.warning("啊咧?记事本被弄脏了,正在重建记事本......")
self.store = {}
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump({}, f, ensure_ascii=False, indent=4)
logger.success("记事本重建成功!")
else:
# 不存在本地存储文件,创建新的目录和文件
logger.warning("啊咧?记事本不存在,正在创建新的记事本......")
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump({}, f, ensure_ascii=False, indent=4)
logger.success("记事本创建成功!")
def save_local_store(self):
"""保存本地存储数据"""
logger.debug(f"保存本地存储数据: {self.file_path}")
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump(self.store, f, ensure_ascii=False, indent=4)
local_storage = LocalStoreManager("data/local_store.json") # 全局单例化

296
src/manager/mood_manager.py Normal file
View File

@@ -0,0 +1,296 @@
import asyncio
import math
import time
from dataclasses import dataclass
from typing import Dict, Tuple
from ..config.config import global_config
from ..common.logger_manager import get_logger
from ..manager.async_task_manager import AsyncTask
from ..individuality.individuality import Individuality
logger = get_logger("mood")
@dataclass
class MoodState:
valence: float
"""愉悦度 (-1.0 到 1.0)-1表示极度负面1表示极度正面"""
arousal: float
"""唤醒度 (-1.0 到 1.0)-1表示抑制1表示兴奋"""
text: str
"""心情的文本描述"""
@dataclass
class MoodChangeHistory:
valence_direction_factor: int
"""愉悦度变化的系数(正为增益,负为抑制)"""
arousal_direction_factor: int
"""唤醒度变化的系数(正为增益,负为抑制)"""
class MoodUpdateTask(AsyncTask):
def __init__(self):
super().__init__(
task_name="Mood Update Task",
wait_before_start=global_config.mood_update_interval,
run_interval=global_config.mood_update_interval,
)
# 从配置文件获取衰减率
self.decay_rate_valence: float = 1 - global_config.mood_decay_rate
"""愉悦度衰减率"""
self.decay_rate_arousal: float = 1 - global_config.mood_decay_rate
"""唤醒度衰减率"""
self.last_update = time.time()
"""上次更新时间"""
async def run(self):
current_time = time.time()
time_diff = current_time - self.last_update
agreeableness_factor = 1 # 宜人性系数
agreeableness_bias = 0 # 宜人性偏置
neuroticism_factor = 0.5 # 神经质系数
# 获取人格特质
personality = Individuality.get_instance().personality
if personality:
# 神经质:影响情绪变化速度
neuroticism_factor = 1 + (personality.neuroticism - 0.5) * 0.4
agreeableness_factor = 1 + (personality.agreeableness - 0.5) * 0.4
# 宜人性:影响情绪基准线
if personality.agreeableness < 0.2:
agreeableness_bias = (personality.agreeableness - 0.2) * 0.5
elif personality.agreeableness > 0.8:
agreeableness_bias = (personality.agreeableness - 0.8) * 0.5
else:
agreeableness_bias = 0
# 分别计算正向和负向的衰减率
if mood_manager.current_mood.valence >= 0:
# 正向情绪衰减
decay_rate_positive = self.decay_rate_valence * (1 / agreeableness_factor)
valence_target = 0 + agreeableness_bias
new_valence = valence_target + (mood_manager.current_mood.valence - valence_target) * math.exp(
-decay_rate_positive * time_diff * neuroticism_factor
)
else:
# 负向情绪衰减
decay_rate_negative = self.decay_rate_valence * agreeableness_factor
valence_target = 0 + agreeableness_bias
new_valence = valence_target + (mood_manager.current_mood.valence - valence_target) * math.exp(
-decay_rate_negative * time_diff * neuroticism_factor
)
# Arousal 向中性0回归
arousal_target = 0
new_arousal = arousal_target + (mood_manager.current_mood.arousal - arousal_target) * math.exp(
-self.decay_rate_arousal * time_diff * neuroticism_factor
)
mood_manager.set_current_mood(new_valence, new_arousal)
self.last_update = current_time
class MoodPrintTask(AsyncTask):
def __init__(self):
super().__init__(
task_name="Mood Print Task",
wait_before_start=60,
run_interval=60,
)
async def run(self):
# 打印当前心情
logger.info(
f"愉悦度: {mood_manager.current_mood.valence:.2f}, "
f"唤醒度: {mood_manager.current_mood.arousal:.2f}, "
f"心情: {mood_manager.current_mood.text}"
)
class MoodManager:
# TODO: 改进,使用具有实验支持的新情绪模型
EMOTION_FACTOR_MAP: Dict[str, Tuple[float, float]] = {
"开心": (0.21, 0.6),
"害羞": (0.15, 0.2),
"愤怒": (-0.24, 0.8),
"恐惧": (-0.21, 0.7),
"悲伤": (-0.21, 0.3),
"厌恶": (-0.12, 0.4),
"惊讶": (0.06, 0.7),
"困惑": (0.0, 0.6),
"平静": (0.03, 0.5),
}
"""
情绪词映射表 {mood: (valence, arousal)}
将情绪描述词映射到愉悦度和唤醒度的元组
"""
EMOTION_POINT_MAP: Dict[Tuple[float, float], str] = {
# 第一象限:高唤醒,正愉悦
(0.5, 0.4): "兴奋",
(0.3, 0.6): "快乐",
(0.2, 0.3): "满足",
# 第二象限:高唤醒,负愉悦
(-0.5, 0.4): "愤怒",
(-0.3, 0.6): "焦虑",
(-0.2, 0.3): "烦躁",
# 第三象限:低唤醒,负愉悦
(-0.5, -0.4): "悲伤",
(-0.3, -0.3): "疲倦",
(-0.4, -0.7): "疲倦",
# 第四象限:低唤醒,正愉悦
(0.2, -0.1): "平静",
(0.3, -0.2): "安宁",
(0.5, -0.4): "放松",
}
"""
情绪文本映射表 {(valence, arousal): mood}
将量化的情绪状态元组映射到文本描述
"""
def __init__(self):
self.current_mood = MoodState(
valence=0.0,
arousal=0.0,
text="平静",
)
"""当前情绪状态"""
self.mood_change_history: MoodChangeHistory = MoodChangeHistory(
valence_direction_factor=0,
arousal_direction_factor=0,
)
"""情绪变化历史"""
self._lock = asyncio.Lock()
"""异步锁,用于保护线程安全"""
def set_current_mood(self, new_valence: float, new_arousal: float):
"""
设置当前情绪状态
:param new_valence: 新的愉悦度
:param new_arousal: 新的唤醒度
"""
# 限制范围
self.current_mood.valence = max(-1.0, min(new_valence, 1.0))
self.current_mood.arousal = max(-1.0, min(new_arousal, 1.0))
closest_mood = None
min_distance = float("inf")
for (v, a), text in self.EMOTION_POINT_MAP.items():
# 计算当前情绪状态与每个情绪文本的欧氏距离
distance = math.sqrt((self.current_mood.valence - v) ** 2 + (self.current_mood.arousal - a) ** 2)
if distance < min_distance:
min_distance = distance
closest_mood = text
if closest_mood:
self.current_mood.text = closest_mood
def update_current_mood(self, valence_delta: float, arousal_delta: float):
"""
根据愉悦度和唤醒度变化量更新当前情绪状态
:param valence_delta: 愉悦度变化量
:param arousal_delta: 唤醒度变化量
"""
# 计算连续增益/抑制
# 规则:多次相同方向的变化会有更大的影响系数,反方向的变化会清零影响系数(系数的正负号由变化方向决定)
if valence_delta * self.mood_change_history.valence_direction_factor > 0:
# 如果方向相同,则根据变化方向改变系数
if valence_delta > 0:
self.mood_change_history.valence_direction_factor += 1 # 若为正向,则增加
else:
self.mood_change_history.valence_direction_factor -= 1 # 若为负向,则减少
else:
# 如果方向不同,则重置计数
self.mood_change_history.valence_direction_factor = 0
if arousal_delta * self.mood_change_history.arousal_direction_factor > 0:
# 如果方向相同,则根据变化方向改变系数
if arousal_delta > 0:
self.mood_change_history.arousal_direction_factor += 1 # 若为正向,则增加计数
else:
self.mood_change_history.arousal_direction_factor -= 1 # 若为负向,则减少计数
else:
# 如果方向不同,则重置计数
self.mood_change_history.arousal_direction_factor = 0
# 计算增益/抑制的结果
# 规则:如果当前情绪状态与变化方向相同,则增益;否则抑制
if self.current_mood.valence * self.mood_change_history.valence_direction_factor > 0:
valence_delta = valence_delta * (1.01 ** abs(self.mood_change_history.valence_direction_factor))
else:
valence_delta = valence_delta * (0.99 ** abs(self.mood_change_history.valence_direction_factor))
if self.current_mood.arousal * self.mood_change_history.arousal_direction_factor > 0:
arousal_delta = arousal_delta * (1.01 ** abs(self.mood_change_history.arousal_direction_factor))
else:
arousal_delta = arousal_delta * (0.99 ** abs(self.mood_change_history.arousal_direction_factor))
self.set_current_mood(
new_valence=self.current_mood.valence + valence_delta,
new_arousal=self.current_mood.arousal + arousal_delta,
)
def get_mood_prompt(self) -> str:
"""
根据当前情绪状态生成提示词
"""
base_prompt = f"当前心情:{self.current_mood.text}"
# 根据情绪状态添加额外的提示信息
if self.current_mood.valence > 0.5:
base_prompt += "你现在心情很好,"
elif self.current_mood.valence < -0.5:
base_prompt += "你现在心情不太好,"
if self.current_mood.arousal > 0.4:
base_prompt += "情绪比较激动。"
elif self.current_mood.arousal < -0.4:
base_prompt += "情绪比较平静。"
return base_prompt
def get_arousal_multiplier(self) -> float:
"""
根据当前情绪状态返回唤醒度乘数
"""
if self.current_mood.arousal > 0.4:
multiplier = 1 + min(0.15, (self.current_mood.arousal - 0.4) / 3)
return multiplier
elif self.current_mood.arousal < -0.4:
multiplier = 1 - min(0.15, ((0 - self.current_mood.arousal) - 0.4) / 3)
return multiplier
return 1.0
def update_mood_from_emotion(self, emotion: str, intensity: float = 1.0) -> None:
"""
根据情绪词更新心情状态
:param emotion: 情绪词(如'开心', '悲伤'等位于self.EMOTION_FACTOR_MAP中的键
:param intensity: 情绪强度0.0-1.0
"""
if emotion not in self.EMOTION_FACTOR_MAP:
logger.error(f"[情绪更新] 未知情绪词: {emotion}")
return
valence_change, arousal_change = self.EMOTION_FACTOR_MAP[emotion]
old_valence = self.current_mood.valence
old_arousal = self.current_mood.arousal
old_mood = self.current_mood.text
self.update_current_mood(valence_change, arousal_change) # 更新当前情绪状态
logger.info(
f"[情绪变化] {emotion}(强度:{intensity:.2f}) | 愉悦度:{old_valence:.2f}->{self.current_mood.valence:.2f}, 唤醒度:{old_arousal:.2f}->{self.current_mood.arousal:.2f} | 心情:{old_mood}->{self.current_mood.text}"
)
mood_manager = MoodManager()
"""全局情绪管理器"""

View File

@@ -6,7 +6,6 @@ MaiMBot插件系统
from .chat.chat_stream import chat_manager from .chat.chat_stream import chat_manager
from .emoji_system.emoji_manager import emoji_manager from .emoji_system.emoji_manager import emoji_manager
from .person_info.relationship_manager import relationship_manager from .person_info.relationship_manager import relationship_manager
from .moods.moods import MoodManager
from .willing.willing_manager import willing_manager from .willing.willing_manager import willing_manager
from .schedule.schedule_generator import bot_schedule from .schedule.schedule_generator import bot_schedule
@@ -15,7 +14,6 @@ __all__ = [
"chat_manager", "chat_manager",
"emoji_manager", "emoji_manager",
"relationship_manager", "relationship_manager",
"MoodManager",
"willing_manager", "willing_manager",
"bot_schedule", "bot_schedule",
] ]

View File

@@ -1,16 +1,15 @@
import traceback
from typing import Dict, Any from typing import Dict, Any
from ..moods.moods import MoodManager # 导入情绪管理器
from ...config.config import global_config
from .message import MessageRecv
from ..PFC.pfc_manager import PFCManager
from .chat_stream import chat_manager
from .only_message_process import MessageProcessor
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
from src.manager.mood_manager import mood_manager # 导入情绪管理器
from .chat_stream import chat_manager
from .message import MessageRecv
from .only_message_process import MessageProcessor
from ..PFC.pfc_manager import PFCManager
from ..heartFC_chat.heartflow_processor import HeartFCProcessor from ..heartFC_chat.heartflow_processor import HeartFCProcessor
from ..utils.prompt_builder import Prompt, global_prompt_manager from ..utils.prompt_builder import Prompt, global_prompt_manager
import traceback from ...config.config import global_config
# 定义日志配置 # 定义日志配置
@@ -23,7 +22,7 @@ class ChatBot:
def __init__(self): def __init__(self):
self.bot = None # bot 实例引用 self.bot = None # bot 实例引用
self._started = False self._started = False
self.mood_manager = MoodManager.get_instance() # 获取情绪管理器单例 self.mood_manager = mood_manager # 获取情绪管理器单例
self.heartflow_processor = HeartFCProcessor() # 新增 self.heartflow_processor = HeartFCProcessor() # 新增
# 创建初始化PFC管理器的任务会在_ensure_started时执行 # 创建初始化PFC管理器的任务会在_ensure_started时执行

View File

@@ -1,21 +1,20 @@
import random import random
import time
import re import re
import time
from collections import Counter from collections import Counter
import jieba import jieba
import numpy as np import numpy as np
from src.common.logger import get_module_logger from maim_message import UserInfo
from pymongo.errors import PyMongoError from pymongo.errors import PyMongoError
from src.common.logger import get_module_logger
from src.manager.mood_manager import mood_manager
from .message import MessageRecv
from ..models.utils_model import LLMRequest from ..models.utils_model import LLMRequest
from ..utils.typo_generator import ChineseTypoGenerator from ..utils.typo_generator import ChineseTypoGenerator
from ...config.config import global_config
from .message import MessageRecv
from maim_message import UserInfo
from ..moods.moods import MoodManager
from ...common.database import db from ...common.database import db
from ...config.config import global_config
logger = get_module_logger("chat_utils") logger = get_module_logger("chat_utils")
@@ -405,7 +404,6 @@ def calculate_typing_time(
- 在所有输入结束后额外加上回车时间0.3秒 - 在所有输入结束后额外加上回车时间0.3秒
- 如果is_emoji为True将使用固定1秒的输入时间 - 如果is_emoji为True将使用固定1秒的输入时间
""" """
mood_manager = MoodManager.get_instance()
# 将0-1的唤醒度映射到-1到1 # 将0-1的唤醒度映射到-1到1
mood_arousal = mood_manager.current_mood.arousal mood_arousal = mood_manager.current_mood.arousal
# 映射到0.5到2倍的速度系数 # 映射到0.5到2倍的速度系数

View File

@@ -1,33 +1,35 @@
import asyncio import asyncio
import contextlib
import json # <--- 确保导入 json
import random # <--- 添加导入
import time import time
import traceback import traceback
import random # <--- 添加导入
import json # <--- 确保导入 json
from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine
from collections import deque from collections import deque
from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine
from rich.traceback import install
from src.common.logger_manager import get_logger
from src.config.config import global_config
from src.heart_flow.observation import Observation
from src.heart_flow.sub_mind import SubMind
from src.heart_flow.utils_chat import get_chat_type_and_target_info
from src.manager.mood_manager import mood_manager
from src.plugins.chat.chat_stream import ChatStream
from src.plugins.chat.chat_stream import chat_manager
from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending
from src.plugins.chat.message import Seg # Local import needed after move from src.plugins.chat.message import Seg # Local import needed after move
from src.plugins.chat.chat_stream import ChatStream
from src.plugins.chat.message import UserInfo from src.plugins.chat.message import UserInfo
from src.plugins.chat.chat_stream import chat_manager
from src.common.logger_manager import get_logger
from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config
from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move
from src.plugins.utils.timer_calculator import Timer # <--- Import Timer
from src.plugins.emoji_system.emoji_manager import emoji_manager
from src.heart_flow.sub_mind import SubMind
from src.heart_flow.observation import Observation
from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder
import contextlib
from src.plugins.utils.chat_message_builder import num_new_messages_since
from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo
from .heartFC_sender import HeartFCSender
from src.plugins.chat.utils import process_llm_response from src.plugins.chat.utils import process_llm_response
from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move
from src.plugins.emoji_system.emoji_manager import emoji_manager
from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo
from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder
from src.plugins.models.utils_model import LLMRequest
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
from src.plugins.moods.moods import MoodManager from src.plugins.utils.chat_message_builder import num_new_messages_since
from src.heart_flow.utils_chat import get_chat_type_and_target_info from src.plugins.utils.timer_calculator import Timer # <--- Import Timer
from rich.traceback import install from .heartFC_sender import HeartFCSender
install(extra_lines=3) install(extra_lines=3)
@@ -1275,7 +1277,7 @@ class HeartFChatting:
""" """
try: try:
# 1. 获取情绪影响因子并调整模型温度 # 1. 获取情绪影响因子并调整模型温度
arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier() arousal_multiplier = mood_manager.get_arousal_multiplier()
current_temp = global_config.llm_normal["temp"] * arousal_multiplier current_temp = global_config.llm_normal["temp"] * arousal_multiplier
self.model_normal.temperature = current_temp # 动态调整温度 self.model_normal.temperature = current_temp # 动态调整温度

View File

@@ -10,7 +10,7 @@ import time
from typing import Union, Optional, Deque, Dict, Any from typing import Union, Optional, Deque, Dict, Any
from ...common.database import db from ...common.database import db
from ..chat.utils import get_recent_group_speaker from ..chat.utils import get_recent_group_speaker
from ..moods.moods import MoodManager from src.manager.mood_manager import mood_manager
from ..memory_system.Hippocampus import HippocampusManager from ..memory_system.Hippocampus import HippocampusManager
from ..schedule.schedule_generator import bot_schedule from ..schedule.schedule_generator import bot_schedule
from ..knowledge.knowledge_lib import qa_manager from ..knowledge.knowledge_lib import qa_manager
@@ -341,8 +341,7 @@ class PromptBuilder:
else: else:
logger.warning(f"Invalid person tuple encountered for relationship prompt: {person}") logger.warning(f"Invalid person tuple encountered for relationship prompt: {person}")
mood_manager = MoodManager.get_instance() mood_prompt = mood_manager.get_mood_prompt()
mood_prompt = mood_manager.get_prompt()
reply_styles1 = [ reply_styles1 = [
("然后给出日常且口语化的回复,平淡一些", 0.4), ("然后给出日常且口语化的回复,平淡一些", 0.4),
("给出非常简短的回复", 0.4), ("给出非常简短的回复", 0.4),

View File

@@ -1,26 +1,26 @@
import time
import asyncio import asyncio
import traceback
import statistics # 导入 statistics 模块 import statistics # 导入 statistics 模块
import time
import traceback
from random import random from random import random
from typing import List, Optional # 导入 Optional from typing import List, Optional # 导入 Optional
from ..moods.moods import MoodManager
from ...config.config import global_config
from ..emoji_system.emoji_manager import emoji_manager
from .normal_chat_generator import NormalChatGenerator
from ..chat.message import MessageSending, MessageRecv, MessageThinking, MessageSet
from ..chat.message_sender import message_manager
from ..chat.utils_image import image_path_to_base64
from ..willing.willing_manager import willing_manager
from maim_message import UserInfo, Seg from maim_message import UserInfo, Seg
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
from src.heart_flow.utils_chat import get_chat_type_and_target_info
from src.manager.mood_manager import mood_manager
from src.plugins.chat.chat_stream import ChatStream, chat_manager from src.plugins.chat.chat_stream import ChatStream, chat_manager
from src.plugins.person_info.relationship_manager import relationship_manager from src.plugins.person_info.relationship_manager import relationship_manager
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
from src.plugins.utils.timer_calculator import Timer from src.plugins.utils.timer_calculator import Timer
from src.heart_flow.utils_chat import get_chat_type_and_target_info from .normal_chat_generator import NormalChatGenerator
from ..chat.message import MessageSending, MessageRecv, MessageThinking, MessageSet
from ..chat.message_sender import message_manager
from ..chat.utils_image import image_path_to_base64
from ..emoji_system.emoji_manager import emoji_manager
from ..willing.willing_manager import willing_manager
from ...config.config import global_config
logger = get_logger("chat") logger = get_logger("chat")
@@ -45,7 +45,7 @@ class NormalChat:
# Other sync initializations # Other sync initializations
self.gpt = NormalChatGenerator() self.gpt = NormalChatGenerator()
self.mood_manager = MoodManager.get_instance() self.mood_manager = mood_manager
self.start_time = time.time() self.start_time = time.time()
self.last_speak_time = 0 self.last_speak_time = 0
self._chat_task: Optional[asyncio.Task] = None self._chat_task: Optional[asyncio.Task] = None

View File

@@ -1,293 +0,0 @@
import math
import threading
import time
from dataclasses import dataclass
from ...config.config import global_config
from src.common.logger_manager import get_logger
from ..person_info.relationship_manager import relationship_manager
from src.individuality.individuality import Individuality
logger = get_logger("mood")
@dataclass
class MoodState:
valence: float # 愉悦度 (-1.0 到 1.0)-1表示极度负面1表示极度正面
arousal: float # 唤醒度 (-1.0 到 1.0)-1表示抑制1表示兴奋
text: str # 心情文本描述
class MoodManager:
_instance = None
_lock = threading.Lock()
def __new__(cls):
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
# 确保初始化代码只运行一次
if self._initialized:
return
self._initialized = True
# 初始化心情状态
self.current_mood = MoodState(valence=0.0, arousal=0.0, text="平静")
# 从配置文件获取衰减率
self.decay_rate_valence = 1 - global_config.mood_decay_rate # 愉悦度衰减率
self.decay_rate_arousal = 1 - global_config.mood_decay_rate # 唤醒度衰减率
# 上次更新时间
self.last_update = time.time()
# 线程控制
self._running = False
self._update_thread = None
# 情绪词映射表 (valence, arousal)
self.emotion_map = {
"开心": (0.21, 0.6),
"害羞": (0.15, 0.2),
"愤怒": (-0.24, 0.8),
"恐惧": (-0.21, 0.7),
"悲伤": (-0.21, 0.3),
"厌恶": (-0.12, 0.4),
"惊讶": (0.06, 0.7),
"困惑": (0.0, 0.6),
"平静": (0.03, 0.5),
}
# 情绪文本映射表
self.mood_text_map = {
# 第一象限:高唤醒,正愉悦
(0.5, 0.4): "兴奋",
(0.3, 0.6): "快乐",
(0.2, 0.3): "满足",
# 第二象限:高唤醒,负愉悦
(-0.5, 0.4): "愤怒",
(-0.3, 0.6): "焦虑",
(-0.2, 0.3): "烦躁",
# 第三象限:低唤醒,负愉悦
(-0.5, -0.4): "悲伤",
(-0.3, -0.3): "疲倦",
(-0.4, -0.7): "疲倦",
# 第四象限:低唤醒,正愉悦
(0.2, -0.1): "平静",
(0.3, -0.2): "安宁",
(0.5, -0.4): "放松",
}
@classmethod
def get_instance(cls) -> "MoodManager":
"""获取MoodManager的单例实例"""
if cls._instance is None:
cls._instance = MoodManager()
return cls._instance
def start_mood_update(self, update_interval: float = 5.0) -> None:
"""
启动情绪更新线程
:param update_interval: 更新间隔(秒)
"""
if self._running:
return
self._running = True
self._update_thread = threading.Thread(
target=self._continuous_mood_update, args=(update_interval,), daemon=True
)
self._update_thread.start()
def stop_mood_update(self) -> None:
"""停止情绪更新线程"""
self._running = False
if self._update_thread and self._update_thread.is_alive():
self._update_thread.join()
def _continuous_mood_update(self, update_interval: float) -> None:
"""
持续更新情绪状态的线程函数
:param update_interval: 更新间隔(秒)
"""
while self._running:
self._apply_decay()
self._update_mood_text()
time.sleep(update_interval)
def _apply_decay(self) -> None:
"""应用情绪衰减,正向和负向情绪分开计算"""
current_time = time.time()
time_diff = current_time - self.last_update
agreeableness_factor = 1
agreeableness_bias = 0
neuroticism_factor = 0.5
# 获取人格特质
personality = Individuality.get_instance().personality
if personality:
# 神经质:影响情绪变化速度
neuroticism_factor = 1 + (personality.neuroticism - 0.5) * 0.4
agreeableness_factor = 1 + (personality.agreeableness - 0.5) * 0.4
# 宜人性:影响情绪基准线
if personality.agreeableness < 0.2:
agreeableness_bias = (personality.agreeableness - 0.2) * 0.5
elif personality.agreeableness > 0.8:
agreeableness_bias = (personality.agreeableness - 0.8) * 0.5
else:
agreeableness_bias = 0
# 分别计算正向和负向的衰减率
if self.current_mood.valence >= 0:
# 正向情绪衰减
decay_rate_positive = self.decay_rate_valence * (1 / agreeableness_factor)
valence_target = 0 + agreeableness_bias
self.current_mood.valence = valence_target + (self.current_mood.valence - valence_target) * math.exp(
-decay_rate_positive * time_diff * neuroticism_factor
)
else:
# 负向情绪衰减
decay_rate_negative = self.decay_rate_valence * agreeableness_factor
valence_target = 0 + agreeableness_bias
self.current_mood.valence = valence_target + (self.current_mood.valence - valence_target) * math.exp(
-decay_rate_negative * time_diff * neuroticism_factor
)
# Arousal 向中性0回归
arousal_target = 0
self.current_mood.arousal = arousal_target + (self.current_mood.arousal - arousal_target) * math.exp(
-self.decay_rate_arousal * time_diff * neuroticism_factor
)
# 确保值在合理范围内
self.current_mood.valence = max(-1.0, min(1.0, self.current_mood.valence))
self.current_mood.arousal = max(-1.0, min(1.0, self.current_mood.arousal))
self.last_update = current_time
def update_mood_from_text(self, text: str, valence_change: float, arousal_change: float) -> None:
"""根据输入文本更新情绪状态"""
self.current_mood.valence += valence_change
self.current_mood.arousal += arousal_change
# 限制范围
self.current_mood.valence = max(-1.0, min(1.0, self.current_mood.valence))
self.current_mood.arousal = max(-1.0, min(1.0, self.current_mood.arousal))
self._update_mood_text()
def set_mood_text(self, text: str) -> None:
"""直接设置心情文本"""
self.current_mood.text = text
def _update_mood_text(self) -> None:
"""根据当前情绪状态更新文本描述"""
closest_mood = None
min_distance = float("inf")
for (v, a), text in self.mood_text_map.items():
distance = math.sqrt((self.current_mood.valence - v) ** 2 + (self.current_mood.arousal - a) ** 2)
if distance < min_distance:
min_distance = distance
closest_mood = text
if closest_mood:
self.current_mood.text = closest_mood
def update_mood_by_user(self, user_id: str, valence_change: float, arousal_change: float) -> None:
"""根据用户ID更新情绪状态"""
# 这里可以根据用户ID添加特定的权重或规则
weight = 1.0 # 默认权重
self.current_mood.valence += valence_change * weight
self.current_mood.arousal += arousal_change * weight
# 限制范围
self.current_mood.valence = max(-1.0, min(1.0, self.current_mood.valence))
self.current_mood.arousal = max(-1.0, min(1.0, self.current_mood.arousal))
self._update_mood_text()
def get_prompt(self) -> str:
"""根据当前情绪状态生成提示词"""
base_prompt = f"当前心情:{self.current_mood.text}"
# 根据情绪状态添加额外的提示信息
if self.current_mood.valence > 0.5:
base_prompt += "你现在心情很好,"
elif self.current_mood.valence < -0.5:
base_prompt += "你现在心情不太好,"
if self.current_mood.arousal > 0.4:
base_prompt += "情绪比较激动。"
elif self.current_mood.arousal < -0.4:
base_prompt += "情绪比较平静。"
return base_prompt
def get_arousal_multiplier(self) -> float:
"""根据当前情绪状态返回唤醒度乘数"""
if self.current_mood.arousal > 0.4:
multiplier = 1 + min(0.15, (self.current_mood.arousal - 0.4) / 3)
return multiplier
elif self.current_mood.arousal < -0.4:
multiplier = 1 - min(0.15, ((0 - self.current_mood.arousal) - 0.4) / 3)
return multiplier
return 1.0
def get_current_mood(self) -> MoodState:
"""获取当前情绪状态"""
return self.current_mood
def print_mood_status(self) -> None:
"""打印当前情绪状态"""
logger.info(
f"愉悦度: {self.current_mood.valence:.2f}, "
f"唤醒度: {self.current_mood.arousal:.2f}, "
f"心情: {self.current_mood.text}"
)
def update_mood_from_emotion(self, emotion: str, intensity: float = 1.0) -> None:
"""
根据情绪词更新心情状态
:param emotion: 情绪词(如'happy', 'sad'等)
:param intensity: 情绪强度0.0-1.0
"""
if emotion not in self.emotion_map:
logger.debug(f"[情绪更新] 未知情绪词: {emotion}")
return
valence_change, arousal_change = self.emotion_map[emotion]
old_valence = self.current_mood.valence
old_arousal = self.current_mood.arousal
old_mood = self.current_mood.text
valence_change = relationship_manager.feedback_to_mood(valence_change)
# 应用情绪强度
valence_change *= intensity
arousal_change *= intensity
# 更新当前情绪状态
self.current_mood.valence += valence_change
self.current_mood.arousal += arousal_change
# 限制范围
self.current_mood.valence = max(-1.0, min(1.0, self.current_mood.valence))
self.current_mood.arousal = max(-1.0, min(1.0, self.current_mood.arousal))
self._update_mood_text()
logger.info(
f"[情绪变化] {emotion}(强度:{intensity:.2f}) | 愉悦度:{old_valence:.2f}->{self.current_mood.valence:.2f}, 唤醒度:{old_arousal:.2f}->{self.current_mood.arousal:.2f} | 心情:{old_mood}->{self.current_mood.text}"
)

View File

@@ -6,6 +6,9 @@ from .person_info import person_info_manager
import time import time
import random import random
from maim_message import UserInfo from maim_message import UserInfo
from ...manager.mood_manager import mood_manager
# import re # import re
# import traceback # import traceback
@@ -22,9 +25,7 @@ class RelationshipManager:
@property @property
def mood_manager(self): def mood_manager(self):
if self._mood_manager is None: if self._mood_manager is None:
from ..moods.moods import MoodManager # 延迟导入 self._mood_manager = mood_manager
self._mood_manager = MoodManager.get_instance()
return self._mood_manager return self._mood_manager
def positive_feedback_sys(self, label: str, stance: str): def positive_feedback_sys(self, label: str, stance: str):
@@ -60,9 +61,7 @@ class RelationshipManager:
def mood_feedback(self, value): def mood_feedback(self, value):
"""情绪反馈""" """情绪反馈"""
mood_manager = self.mood_manager mood_manager = self.mood_manager
mood_gain = mood_manager.get_current_mood().valence ** 2 * math.copysign( mood_gain = mood_manager.current_mood.valence**2 * math.copysign(1, value * mood_manager.current_mood.valence)
1, value * mood_manager.get_current_mood().valence
)
value += value * mood_gain value += value * mood_gain
logger.info(f"当前relationship增益系数{mood_gain:.3f}") logger.info(f"当前relationship增益系数{mood_gain:.3f}")
return value return value

View File

@@ -1,4 +0,0 @@
from .remote import main
# 启动心跳线程
heartbeat_thread = main()

View File

@@ -1,248 +1,142 @@
import asyncio
import requests import requests
import time
import uuid
import platform import platform
import os
import json
import threading
import subprocess
# from loguru import logger # from loguru import logger
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
from src.config.config import global_config from src.config.config import global_config
from src.manager.async_task_manager import AsyncTask
from src.manager.local_store_manager import local_storage
logger = get_logger("remote") logger = get_logger("remote")
# --- 使用向上导航的方式定义路径 --- TELEMETRY_SERVER_URL = "http://localhost:8080"
"""遥测服务地址"""
# 1. 获取当前文件 (remote.py) 所在的目录
current_dir = os.path.dirname(os.path.abspath(__file__))
# 2. 从当前目录向上导航三级找到项目根目录
# (src/plugins/remote/ -> src/plugins/ -> src/ -> project_root)
root_dir = os.path.abspath(os.path.join(current_dir, "..", "..", ".."))
# 3. 定义 data 目录的路径 (位于项目根目录下)
data_dir = os.path.join(root_dir, "data")
# 4. 定义 UUID 文件在 data 目录下的完整路径
UUID_FILE = os.path.join(data_dir, "client_uuid.json")
# --- 路径定义结束 ---
# 生成或获取客户端唯一ID class TelemetryHeartBeatTask(AsyncTask):
def get_unique_id(): HEARTBEAT_INTERVAL = 300
# --- 在尝试读写 UUID_FILE 之前确保 data 目录存在 ---
# 将目录检查和创建逻辑移到这里,在首次需要写入前执行
try:
# exist_ok=True 意味着如果目录已存在也不会报错
os.makedirs(data_dir, exist_ok=True)
except OSError as e:
# 处理可能的权限错误等
logger.error(f"无法创建数据目录 {data_dir}: {e}")
# 根据你的错误处理逻辑,可能需要在这里返回错误或抛出异常
# 暂且返回 None 或抛出,避免继续执行导致问题
raise RuntimeError(f"无法创建必要的数据目录 {data_dir}") from e
# --- 目录检查结束 ---
# 检查是否已经有保存的UUID def __init__(self):
if os.path.exists(UUID_FILE): super().__init__(task_name="Telemetry Heart Beat Task", run_interval=self.HEARTBEAT_INTERVAL)
try: self.server_url = TELEMETRY_SERVER_URL
with open(UUID_FILE, "r", encoding="utf-8") as f: # 指定 encoding """遥测服务地址"""
data = json.load(f)
if "client_id" in data:
logger.debug(f"从本地文件读取客户端ID: {UUID_FILE}")
return data["client_id"]
except (json.JSONDecodeError, IOError) as e:
logger.warning(f"读取UUID文件 {UUID_FILE} 出错: {e}将生成新的UUID")
except Exception as e: # 捕捉其他可能的异常
logger.error(f"读取UUID文件 {UUID_FILE} 时发生未知错误: {e}")
# 如果没有保存的UUID或读取出错则生成新的 self.client_uuid = local_storage["mmc_uuid"] if "mmc_uuid" in local_storage else None
client_id = generate_unique_id() """客户端UUID"""
logger.info(f"生成新的客户端ID: {client_id}")
# 保存UUID到文件 self.info_dict = self._get_sys_info()
try: """系统信息字典"""
# 再次确认目录存在 (虽然理论上前面已创建,但更保险)
os.makedirs(data_dir, exist_ok=True)
with open(UUID_FILE, "w", encoding="utf-8") as f: # 指定 encoding
json.dump({"client_id": client_id}, f, indent=4) # 添加 indent 使json可读
logger.info(f"已保存新生成的客户端ID到本地文件: {UUID_FILE}")
except IOError as e:
logger.error(f"保存UUID时出错: {UUID_FILE} - {e}")
except Exception as e: # 捕捉其他可能的异常
logger.error(f"保存UUID文件 {UUID_FILE} 时发生未知错误: {e}")
return client_id @staticmethod
def _get_sys_info() -> dict[str, str]:
"""获取系统信息"""
info_dict = {
"os_type": "Unknown",
"py_version": platform.python_version(),
"mmc_version": global_config.MAI_VERSION,
}
match platform.system():
case "Windows":
info_dict["os_type"] = "Windows"
case "Linux":
info_dict["os_type"] = "Linux"
case "Darwin":
info_dict["os_type"] = "macOS"
case _:
info_dict["os_type"] = "Unknown"
# 生成客户端唯一ID return info_dict
def generate_unique_id():
# 基于机器码生成唯一ID同一台机器上生成的UUID是固定的只要机器码不变
import hashlib
system_info = platform.system() async def _req_uuid(self) -> bool:
machine_code = None """
向服务端请求UUID不应在已存在UUID的情况下调用会覆盖原有的UUID
"""
try: if "deploy_time" not in local_storage:
if system_info == "Windows": logger.error("本地存储中缺少部署时间无法请求UUID")
# 使用wmic命令获取主机UUID更稳定
result = subprocess.check_output(
"wmic csproduct get uuid", shell=True, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL
)
lines = result.decode(errors="ignore").splitlines()
# 过滤掉空行和表头只取有效UUID
uuids = [line.strip() for line in lines if line.strip() and line.strip().lower() != "uuid"]
if uuids:
uuid_val = uuids[0]
# logger.debug(f"主机UUID: {uuid_val}")
# 增加无效值判断
if uuid_val and uuid_val.lower() not in ["to be filled by o.e.m.", "none", "", "standard"]:
machine_code = uuid_val
elif system_info == "Linux":
# 优先读取 /etc/machine-id其次 /var/lib/dbus/machine-id取第一个非空且内容有效的
for path in ["/etc/machine-id", "/var/lib/dbus/machine-id"]:
if os.path.exists(path):
with open(path, "r") as f:
code = f.read().strip()
# 只要内容非空且不是全0
if code and set(code) != {"0"}:
machine_code = code
break
elif system_info == "Darwin":
# macOS: 使用IOPlatformUUID
result = subprocess.check_output(
"ioreg -rd1 -c IOPlatformExpertDevice | awk '/IOPlatformUUID/'", shell=True
)
uuid_line = result.decode(errors="ignore")
# 解析出 "IOPlatformUUID" = "xxxx-xxxx-xxxx-xxxx"
import re
m = re.search(r'"IOPlatformUUID"\s*=\s*"([^"]+)"', uuid_line)
if m:
uuid_val = m.group(1)
logger.debug(f"IOPlatformUUID: {uuid_val}")
if uuid_val and uuid_val.lower() not in ["to be filled by o.e.m.", "none", "", "standard"]:
machine_code = uuid_val
except Exception as e:
logger.debug(f"获取机器码失败: {e}")
# 如果主板序列号无效尝试用MAC地址
if not machine_code:
try:
mac = uuid.getnode()
if (mac >> 40) % 2 == 0: # 不是本地伪造MAC
machine_code = str(mac)
except Exception as e:
logger.debug(f"获取MAC地址失败: {e}")
def md5_to_uuid(md5hex):
# 将32位md5字符串格式化为8-4-4-4-12的UUID格式
return f"{md5hex[0:8]}-{md5hex[8:12]}-{md5hex[12:16]}-{md5hex[16:20]}-{md5hex[20:32]}"
if machine_code:
# print(f"machine_code={machine_code!r}") # 可用于调试
md5 = hashlib.md5(machine_code.encode("utf-8")).hexdigest()
uuid_str = md5_to_uuid(md5)
else:
uuid_str = str(uuid.uuid4())
unique_id = f"{system_info}-{uuid_str}"
return unique_id
def send_heartbeat(server_url, client_id):
"""向服务器发送心跳"""
sys = platform.system()
try:
headers = {"Client-ID": client_id, "User-Agent": f"HeartbeatClient/{client_id[:8]}"}
data = json.dumps(
{"system": sys, "Version": global_config.MAI_VERSION},
)
logger.debug(f"正在发送心跳到服务器: {server_url}")
logger.debug(f"心跳数据: {data}")
response = requests.post(f"{server_url}/api/clients", headers=headers, data=data)
if response.status_code == 201:
data = response.json()
logger.debug(f"心跳发送成功。服务器响应: {data}")
return True
else:
logger.debug(f"心跳发送失败。状态码: {response.status_code}, 响应内容: {response.text}")
return False return False
except requests.RequestException as e: try_count: int = 0
# 如果请求异常,可能是网络问题,不记录错误 while True:
logger.debug(f"发送心跳时出错: {e}") # 如果不存在则向服务端请求一个新的UUID注册客户端
return False logger.info("正在向遥测服务端请求UUID...")
try:
response = requests.post(
f"{TELEMETRY_SERVER_URL}/stat/reg_client",
json={"deploy_time": local_storage["deploy_time"]},
)
class HeartbeatThread(threading.Thread): if response.status_code == 200:
"""心跳线程类""" data = response.json()
client_id = data.get("mmc_uuid")
if client_id:
# 将UUID存储到本地
local_storage["mmc_uuid"] = client_id
self.client_uuid = client_id
logger.info(f"成功获取UUID: {self.client_uuid}")
return True # 成功获取UUID返回True
else:
logger.error("无效的服务端响应")
else:
logger.error(f"请求UUID失败状态码: {response.status_code}, 响应内容: {response.text}")
except requests.RequestException as e:
logger.error(f"请求UUID时出错: {e}") # 可能是网络问题
def __init__(self, server_url, interval): # 请求失败,重试次数+1
super().__init__(daemon=True) # 设置为守护线程,主程序结束时自动结束 try_count += 1
self.server_url = server_url if try_count > 3:
self.interval = interval # 如果超过3次仍然失败则退出
self.client_id = get_unique_id() logger.error("获取UUID失败请检查网络连接或服务端状态")
self.running = True return False
self.stop_event = threading.Event() # 添加事件对象用于可中断的等待
self.last_heartbeat_time = 0 # 记录上次发送心跳的时间
def run(self):
"""线程运行函数"""
logger.debug(f"心跳线程已启动客户端ID: {self.client_id}")
while self.running:
# 发送心跳
if send_heartbeat(self.server_url, self.client_id):
logger.info(f"{self.interval}秒后发送下一次心跳...")
else: else:
logger.info(f"{self.interval}秒后重试...") # 如果可以重试,等待后继续(指数退避)
await asyncio.sleep(4**try_count)
self.last_heartbeat_time = time.time() async def _send_heartbeat(self):
"""向服务器发送心跳"""
try:
headers = {
"Client-UUID": self.client_uuid,
"User-Agent": f"HeartbeatClient/{self.client_uuid[:8]}",
}
# 使用可中断的等待代替 sleep logger.debug(f"正在发送心跳到服务器: {self.server_url}")
# 每秒检查一次是否应该停止或发送心跳
remaining_wait = self.interval
while remaining_wait > 0 and self.running:
# 每次最多等待1秒便于及时响应停止请求
wait_time = min(1, remaining_wait)
if self.stop_event.wait(wait_time):
break # 如果事件被设置,立即退出等待
remaining_wait -= wait_time
# 检查是否由于外部原因导致间隔异常延长 response = requests.post(
if time.time() - self.last_heartbeat_time >= self.interval * 1.5: f"{self.server_url}/stat/client_heartbeat",
logger.warning("检测到心跳间隔异常延长,立即发送心跳") headers=headers,
break json=self.info_dict,
)
def stop(self): # 处理响应
"""停止线程""" if 200 <= response.status_code < 300:
self.running = False # 成功
self.stop_event.set() # 设置事件,中断等待 logger.debug(f"心跳发送成功,状态码: {response.status_code}")
logger.debug("心跳线程已收到停止信号") elif response.status_code == 403:
# 403 Forbidden
logger.error(
"心跳发送失败403 Forbidden: 可能是UUID无效或未注册。"
"处理措施重置UUID下次发送心跳时将尝试重新注册。"
)
self.client_uuid = None
del local_storage["mmc_uuid"] # 删除本地存储的UUID
else:
# 其他错误
logger.error(f"心跳发送失败,状态码: {response.status_code}, 响应内容: {response.text}")
except requests.RequestException as e:
logger.error(f"心跳发送失败: {e}")
def main(): async def run(self):
if global_config.remote_enable: # 发送心跳
"""主函数,启动心跳线程""" if global_config.remote_enable:
# 配置 if self.client_uuid is None:
server_url = "http://hyybuth.xyz:10058" if not await self._req_uuid():
# server_url = "http://localhost:10058" logger.error("获取UUID失败跳过此次心跳")
heartbeat_interval = 300 # 5分钟 return
# 创建并启动心跳线程 await self._send_heartbeat()
heartbeat_thread = HeartbeatThread(server_url, heartbeat_interval)
heartbeat_thread.start()
return heartbeat_thread # 返回线程对象,便于外部控制
return None
# --- 测试用例 ---
if __name__ == "__main__":
print("测试唯一ID生成")
print("唯一ID:", get_unique_id())

File diff suppressed because it is too large Load Diff