From ebb575ec535f56447e83af4757d76950a1e19627 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 12 Sep 2025 19:04:27 +0800 Subject: [PATCH 01/40] =?UTF-8?q?refactor(config):=20=E7=A7=BB=E9=99=A4.en?= =?UTF-8?q?v=E4=BE=9D=E8=B5=96=E5=B9=B6=E5=BC=95=E5=85=A5=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8=E9=85=8D=E7=BD=AE=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将项目配置系统从依赖.env文件和环境变量迁移到使用Pydantic模型进行集中管理。此举通过移除`python-dotenv`库简化了环境设置,并提高了配置的类型安全性和可维护性。 主要变更包括: - 移除`bot.py`中的.env加载逻辑。 - 新增`ServerConfig`模型来管理服务器的主机和端口。 - 更新`src/common/server.py`和`src/common/message/api.py`以从全局配置对象获取服务器设置,取代了`os.environ`。 - 从配置中移除了已废弃的`MaizoneIntercomConfig`。 - 在`bot_config_template.toml`中添加了新的`[server]`配置部分。 --- bot.py | 9 --------- src/common/message/api.py | 4 ++-- src/common/server.py | 4 ++-- src/config/config.py | 6 ++---- src/config/official_configs.py | 16 +++++++--------- template/bot_config_template.toml | 5 ++++- 6 files changed, 17 insertions(+), 27 deletions(-) diff --git a/bot.py b/bot.py index 51d76e642..aab5cd4f1 100644 --- a/bot.py +++ b/bot.py @@ -7,17 +7,9 @@ import time import platform import traceback from pathlib import Path -from dotenv import load_dotenv from rich.traceback import install from colorama import init, Fore -if os.path.exists(".env"): - load_dotenv(".env", override=True) - print("成功加载环境变量配置") -else: - print("未找到.env文件,请确保程序所需的环境变量被正确设置") - raise FileNotFoundError(".env 文件不存在,请创建并配置所需的环境变量") - # maim_message imports for console input # 最早期初始化日志系统,确保所有后续模块都使用正确的日志格式 @@ -45,7 +37,6 @@ logger.info(f"已设置工作目录为: {script_dir}") confirm_logger = get_logger("confirm") # 获取没有加载env时的环境变量 -env_mask = {key: os.getenv(key) for key in os.environ} uvicorn_server = None driver = None diff --git a/src/common/message/api.py b/src/common/message/api.py index eed85c0a9..a85677f47 100644 --- a/src/common/message/api.py +++ b/src/common/message/api.py @@ -24,8 +24,8 @@ def get_global_api() -> MessageServer: # sourcery skip: extract-method # 设置基本参数 kwargs = { - "host": os.environ["HOST"], - "port": int(os.environ["PORT"]), + "host": global_config.server.host, + "port": int(global_config.server.port), "app": get_global_server().get_app(), } diff --git a/src/common/server.py b/src/common/server.py index 24311e54d..30c55d72d 100644 --- a/src/common/server.py +++ b/src/common/server.py @@ -2,7 +2,7 @@ from fastapi import FastAPI, APIRouter from fastapi.middleware.cors import CORSMiddleware # 新增导入 from typing import Optional from uvicorn import Config, Server as UvicornServer -import os +from src.config.config import global_config from rich.traceback import install install(extra_lines=3) @@ -98,5 +98,5 @@ def get_global_server() -> Server: """获取全局服务器实例""" global global_server if global_server is None: - global_server = Server(host=os.environ["HOST"], port=int(os.environ["PORT"])) + global_server = Server(host=global_config.server.host,port=int(global_config.server.port),) return global_server diff --git a/src/config/config.py b/src/config/config.py index ef2d413dd..a38122300 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -43,8 +43,8 @@ from src.config.official_configs import ( CrossContextConfig, PermissionConfig, CommandConfig, - MaizoneIntercomConfig, PlanningSystemConfig, + ServerConfig, ) from .api_ada_configs import ( @@ -399,9 +399,7 @@ class Config(ValidatedConfigBase): cross_context: CrossContextConfig = Field( default_factory=lambda: CrossContextConfig(), description="跨群聊上下文共享配置" ) - maizone_intercom: MaizoneIntercomConfig = Field( - default_factory=lambda: MaizoneIntercomConfig(), description="Maizone互通组配置" - ) + server: ServerConfig = Field(default_factory=lambda: ServerConfig(), description="主服务器配置") class APIAdapterConfig(ValidatedConfigBase): diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 2252041f3..6a557a342 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -494,6 +494,13 @@ class ExperimentalConfig(ValidatedConfigBase): pfc_chatting: bool = Field(default=False, description="启用PFC聊天") +class ServerConfig(ValidatedConfigBase): + """主服务器配置类""" + + host: str = Field(default="127.0.0.1", description="主服务器监听地址") + port: int = Field(default=8080, description="主服务器监听端口") + + class MaimMessageConfig(ValidatedConfigBase): """maim_message配置类""" @@ -676,15 +683,6 @@ class CrossContextConfig(ValidatedConfigBase): enable: bool = Field(default=False, 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 CommandConfig(ValidatedConfigBase): """命令系统配置类""" diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 5ce2f5797..0570c4ec7 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.8.4" +version = "6.8.5" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -481,6 +481,9 @@ insomnia_duration_minutes = [30, 60] # 单次失眠状态的持续时间范围 # 入睡后,经过一段延迟后触发失眠判定的延迟时间(分钟),设置为范围以增加随机性 insomnia_trigger_delay_minutes = [15, 45] +[server] +host = "127.0.0.1" +port = 8080 [cross_context] # 跨群聊/私聊上下文共享配置 # 这是总开关,用于一键启用或禁用此功能 From eb5011ada1f776ca38622f76ef1dc7cd8a2619e5 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 12 Sep 2025 19:08:16 +0800 Subject: [PATCH 02/40] =?UTF-8?q?refactor(chat):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E5=88=86=E6=AE=B5=E6=8C=87=E4=BB=A4=E5=9C=A8=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E8=AF=8D=E4=B8=AD=E7=9A=84=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将回复分割指令从注入到 "现在,你说:" 之前,改为直接添加到整个提示词的顶部。 这种调整简化了提示词的构建逻辑,并确保分割指令作为最高优先级的上下文被模型处理,从而提高其遵循指令的稳定性。 --- src/chat/replyer/default_generator.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 0217f18e6..d3a80c2a5 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -1038,10 +1038,8 @@ class DefaultReplyer: **任务**: 请结合你的智慧和人设,自然地决定是否需要分段。如果需要,请在最恰当的位置插入 `[SPLIT]` 标记。 """ - # 在 "现在,你说:" 之前插入 - parts = prompt_text.rsplit("现在,你说:", 1) - if len(parts) == 2: - prompt_text = f"{parts[0]}{split_instruction}\n现在,你说:{parts[1]}" + # 将分段指令添加到提示词顶部 + prompt_text = f"{split_instruction}\n{prompt_text}" return prompt_text From 493cd2b837502f09f7490919bdc998e1727ce4b7 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 12 Sep 2025 19:50:20 +0800 Subject: [PATCH 03/40] =?UTF-8?q?refactor(emoji):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=A1=A8=E6=83=85=E5=8F=91=E9=80=81=E9=80=BB=E8=BE=91=E4=B8=BA?= =?UTF-8?q?=E5=9F=BA=E4=BA=8E=E6=83=85=E6=84=9F=E6=A0=87=E7=AD=BE=E9=80=89?= =?UTF-8?q?=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构了`emoji`动作的实现,将原有的LLM选择表情描述的逻辑,改为先由LLM根据聊天内容和理由选择一个最匹配的“情感标签”,然后再从该标签下的表情库中随机选择一个进行发送。 主要变更: - 移除原有的表情抽样、编号和LLM选择编号的复杂流程。 - 引入基于`emotion`元数据的情感标签映射。 - 更新LLM的Prompt,使其专注于选择情感标签而非具体表情。 - 简化了代码逻辑,移除了不再需要的历史记录队列和相关配置项。 - 如果表情没有情感标签或LLM调用失败,则回退到随机发送。 --- src/plugins/built_in/core_actions/emoji.py | 177 ++++++++++----------- 1 file changed, 86 insertions(+), 91 deletions(-) diff --git a/src/plugins/built_in/core_actions/emoji.py b/src/plugins/built_in/core_actions/emoji.py index b3f410a4b..820777b44 100644 --- a/src/plugins/built_in/core_actions/emoji.py +++ b/src/plugins/built_in/core_actions/emoji.py @@ -1,7 +1,5 @@ import random from typing import Tuple -from collections import deque -import json # 导入新插件系统 from src.plugin_system import BaseAction, ActionActivationType, ChatMode @@ -22,7 +20,6 @@ logger = get_logger("emoji") class EmojiAction(BaseAction): """表情动作 - 发送表情包""" - # --- 类级别属性 --- # 激活设置 if global_config.emoji.emoji_activate_type == "llm": activation_type = ActionActivationType.LLM_JUDGE @@ -36,9 +33,6 @@ class EmojiAction(BaseAction): # 动作基本信息 action_name = "emoji" action_description = "发送表情包辅助表达情绪" - - # 最近发送表情的历史记录 - _sent_emoji_history = deque(maxlen=4) # LLM判断提示词 llm_judge_prompt = """ @@ -80,99 +74,100 @@ class EmojiAction(BaseAction): logger.warning(f"{self.log_prefix} 无法获取任何带有描述的有效表情包") return False, "无法获取任何带有描述的有效表情包" - # 3. 根据新配置项决定抽样数量 - sample_size = global_config.emoji.max_context_emojis - if sample_size > 0 and len(all_emojis_obj) > sample_size: - sampled_emojis = random.sample(all_emojis_obj, sample_size) - else: - sampled_emojis = all_emojis_obj # 0表示全部 - - # 4. 为抽样的表情包创建带编号的描述列表 - prompt_emoji_list = [] - for i, emoji in enumerate(sampled_emojis): - prompt_emoji_list.append(f"{i + 1}. {emoji.description}") + # 3. 准备情感数据和后备列表 + emotion_map = {} + all_emojis_data = [] - prompt_emoji_str = "\n".join(prompt_emoji_list) - chosen_emoji_obj: MaiEmoji = None + for emoji in all_emojis_obj: + b64 = image_path_to_base64(emoji.full_path) + if not b64: + continue + + desc = emoji.description + emotions = emoji.emotion + all_emojis_data.append((b64, desc)) - # 5. 获取最近的5条消息内容用于判断 - recent_messages = message_api.get_recent_messages(chat_id=self.chat_id, limit=5) - messages_text = "" - if recent_messages: - messages_text = message_api.build_readable_messages( - messages=recent_messages, - timestamp_mode="normal_no_YMD", - truncate=False, - show_actions=False, + for emo in emotions: + if emo not in emotion_map: + emotion_map[emo] = [] + emotion_map[emo].append((b64, desc)) + + if not all_emojis_data: + logger.warning(f"{self.log_prefix} 无法加载任何有效的表情包数据") + return False, "无法加载任何有效的表情包数据" + + available_emotions = list(emotion_map.keys()) + emoji_base64, emoji_description = "", "" + + if not available_emotions: + logger.warning(f"{self.log_prefix} 获取到的表情包均无情感标签, 将随机发送") + emoji_base64, emoji_description = random.choice(all_emojis_data) + else: + # 获取最近的5条消息内容用于判断 + recent_messages = message_api.get_recent_messages(chat_id=self.chat_id, limit=5) + messages_text = "" + if recent_messages: + messages_text = message_api.build_readable_messages( + messages=recent_messages, + timestamp_mode="normal_no_YMD", + truncate=False, + show_actions=False, + ) + + # 4. 构建prompt让LLM选择情感 + prompt = f""" + 你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个情感标签列表中选择最匹配的一个。 + 这是最近的聊天记录: + {messages_text} + + 这是理由:“{reason}” + 这里是可用的情感标签:{available_emotions} + 请直接返回最匹配的那个情感标签,不要进行任何解释或添加其他多余的文字。 + """ + + if global_config.debug.show_prompt: + logger.info(f"{self.log_prefix} 生成的LLM Prompt: {prompt}") + else: + logger.debug(f"{self.log_prefix} 生成的LLM Prompt: {prompt}") + + # 5. 调用LLM + models = llm_api.get_available_models() + chat_model_config = models.get("planner") + if not chat_model_config: + logger.error(f"{self.log_prefix} 未找到'utils_small'模型配置,无法调用LLM") + return False, "未找到'utils_small'模型配置" + + success, chosen_emotion, _, _ = await llm_api.generate_with_model( + prompt, model_config=chat_model_config, request_type="emoji" ) - # 6. 构建prompt让LLM选择编号 - prompt = f""" - 你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个带编号的表情包描述列表中选择最匹配的 **3个** 表情包,并按匹配度从高到低返回它们的编号。 - 这是最近的聊天记录: - {messages_text} - - 这是理由:“{reason}” - 这里是可用的表情包详细描述列表: - {prompt_emoji_str} - 请直接返回一个包含3个最匹配表情包编号的有序JSON列表,例如:[10, 2, 5],不要进行任何解释或添加其他多余的文字。 - """ - - # 7. 调用LLM - models = llm_api.get_available_models() - chat_model_config = models.get("planner") - if not chat_model_config: - logger.error(f"{self.log_prefix} 未找到 'planner' 模型配置,无法调用LLM") - return False, "未找到 'planner' 模型配置" - - success, chosen_indices_str, _, _ = await llm_api.generate_with_model( - prompt, model_config=chat_model_config, request_type="emoji_selection" - ) - - selected_emoji_obj = None - if success: - try: - chosen_indices = json.loads(chosen_indices_str) - if isinstance(chosen_indices, list): - logger.info(f"{self.log_prefix} LLM选择的表情编号候选项: {chosen_indices}") - for index in chosen_indices: - if isinstance(index, int) and 1 <= index <= len(sampled_emojis): - candidate_emoji = sampled_emojis[index - 1] - if candidate_emoji.hash not in self._sent_emoji_history: - selected_emoji_obj = candidate_emoji - break - else: - logger.warning(f"{self.log_prefix} LLM返回的不是一个列表: {chosen_indices_str}") - except (json.JSONDecodeError, TypeError): - logger.warning(f"{self.log_prefix} 解析LLM返回的编号列表失败: {chosen_indices_str}") - - if selected_emoji_obj: - chosen_emoji_obj = selected_emoji_obj - logger.info(f"{self.log_prefix} 从候选项中选择表情: {chosen_emoji_obj.description}") - else: if not success: - logger.warning(f"{self.log_prefix} LLM调用失败, 将随机选择一个表情包") + logger.warning(f"{self.log_prefix} LLM调用失败: {chosen_emotion}, 将随机选择一个表情包") + emoji_base64, emoji_description = random.choice(all_emojis_data) else: - logger.warning(f"{self.log_prefix} 所有候选项均在最近发送历史中, 将随机选择") - - selectable_emojis = [e for e in all_emojis_obj if e.hash not in self._sent_emoji_history] - if not selectable_emojis: - selectable_emojis = all_emojis_obj - chosen_emoji_obj = random.choice(selectable_emojis) + chosen_emotion = chosen_emotion.strip().replace('"', "").replace("'", "") + logger.info(f"{self.log_prefix} LLM选择的情感: {chosen_emotion}") - # 8. 发送表情包并更新历史记录 - if chosen_emoji_obj: - emoji_base64 = image_path_to_base64(chosen_emoji_obj.full_path) - if emoji_base64: - send_success = await self.send_emoji(emoji_base64) - if send_success: - self._sent_emoji_history.append(chosen_emoji_obj.hash) - logger.info(f"{self.log_prefix} 表情包发送成功: {chosen_emoji_obj.description}") - logger.debug(f"{self.log_prefix} 最近表情历史: {list(self._sent_emoji_history)}") - return True, f"发送表情包: {chosen_emoji_obj.description}" + # 使用模糊匹配来查找最相关的情感标签 + matched_key = next((key for key in emotion_map if chosen_emotion in key), None) + + if matched_key: + emoji_base64, emoji_description = random.choice(emotion_map[matched_key]) + logger.info(f"{self.log_prefix} 找到匹配情感 '{chosen_emotion}' (匹配到: '{matched_key}') 的表情包: {emoji_description}") + else: + logger.warning( + f"{self.log_prefix} LLM选择的情感 '{chosen_emotion}' 不在可用列表中, 将随机选择一个表情包" + ) + emoji_base64, emoji_description = random.choice(all_emojis_data) - logger.error(f"{self.log_prefix} 表情包发送失败") - return False, "表情包发送失败" + # 7. 发送表情包 + success = await self.send_emoji(emoji_base64) + + if not success: + logger.error(f"{self.log_prefix} 表情包发送失败") + return False, "表情包发送失败" + + return True, f"发送表情包: {emoji_description}" except Exception as e: logger.error(f"{self.log_prefix} 表情动作执行失败: {e}", exc_info=True) From d658fd8eb0b533d87a9ee6e94306dc0e7f0cb68e Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 12 Sep 2025 19:54:13 +0800 Subject: [PATCH 04/40] =?UTF-8?q?feat(emoji):=20=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E8=A1=A8=E6=83=85=E5=8C=85=E5=8F=91=E9=80=81=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在表情包发送成功或失败后,分别记录操作信息。这有助于更好地追踪和调试表情包功能的状态,两行就应该能解决重复发送的问题了,遥遥领先! --- src/plugins/built_in/core_actions/emoji.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/built_in/core_actions/emoji.py b/src/plugins/built_in/core_actions/emoji.py index 820777b44..0e3305e6e 100644 --- a/src/plugins/built_in/core_actions/emoji.py +++ b/src/plugins/built_in/core_actions/emoji.py @@ -165,7 +165,9 @@ class EmojiAction(BaseAction): if not success: logger.error(f"{self.log_prefix} 表情包发送失败") + await self.store_action_info(action_build_into_prompt = True,action_prompt_display =f"发送了一个{chosen_emotion}的表情包,但失败了",action_done= False) return False, "表情包发送失败" + await self.store_action_info(action_build_into_prompt = True,action_prompt_display =f"发送了一个{chosen_emotion}的表情包",action_done= True) return True, f"发送表情包: {emoji_description}" From 69a1f60841d9a0a6f28c6f3fe939b2fda42aec48 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 12 Sep 2025 20:34:31 +0800 Subject: [PATCH 05/40] =?UTF-8?q?feat(chat):=20=E5=A2=9E=E5=BC=BAprompt?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=9B=9E=E5=A4=8D=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为HfcContext和ChatStream添加focus_energy配置支持 - 修复默认回复生成器中识别自身消息的逻辑 - 完整实现prompt构建中的表达习惯、记忆、工具信息和知识模块 - 优化错误处理,使用原生异常链式传递 - 确保数据库操作中focus_energy字段的持久化 这些改进提升了聊天系统的上下文感知能力和回复质量,同时增强了模块的健壮性和可维护性。 --- src/chat/chat_loop/hfc_context.py | 4 +- src/chat/message_receive/chat_stream.py | 6 +- src/chat/replyer/default_generator.py | 18 ++- src/chat/utils/prompt.py | 201 ++++++++++++++++++++++-- 4 files changed, 208 insertions(+), 21 deletions(-) diff --git a/src/chat/chat_loop/hfc_context.py b/src/chat/chat_loop/hfc_context.py index e6a4b31f3..fe5d283ae 100644 --- a/src/chat/chat_loop/hfc_context.py +++ b/src/chat/chat_loop/hfc_context.py @@ -5,6 +5,7 @@ from src.person_info.relationship_builder_manager import RelationshipBuilder from src.chat.express.expression_learner import ExpressionLearner from src.chat.planner_actions.action_manager import ActionManager from src.chat.chat_loop.hfc_utils import CycleDetail +from src.config.config import global_config if TYPE_CHECKING: from .sleep_manager.wakeup_manager import WakeUpManager @@ -64,7 +65,8 @@ class HfcContext: self.energy_manager: Optional["EnergyManager"] = None self.sleep_manager: Optional["SleepManager"] = None - self.focus_energy = 1 + # 从聊天流获取focus_energy,如果没有则使用配置文件中的值 + self.focus_energy = getattr(self.chat_stream, "focus_energy", global_config.chat.focus_value) self.no_reply_consecutive = 0 self.total_interest = 0.0 # breaking形式下的累积兴趣值 diff --git a/src/chat/message_receive/chat_stream.py b/src/chat/message_receive/chat_stream.py index c43901eab..f5822acfb 100644 --- a/src/chat/message_receive/chat_stream.py +++ b/src/chat/message_receive/chat_stream.py @@ -83,7 +83,8 @@ class ChatStream: self.sleep_pressure = data.get("sleep_pressure", 0.0) if data else 0.0 self.saved = False self.context: ChatMessageContext = None # type: ignore # 用于存储该聊天的上下文信息 - self.focus_energy = 1 + # 从配置文件中读取focus_value,如果没有则使用默认值1.0 + self.focus_energy = data.get("focus_energy", global_config.chat.focus_value) if data else global_config.chat.focus_value self.no_reply_consecutive = 0 self.breaking_accumulated_interest = 0.0 @@ -98,6 +99,7 @@ class ChatStream: "last_active_time": self.last_active_time, "energy_value": self.energy_value, "sleep_pressure": self.sleep_pressure, + "focus_energy": self.focus_energy, "breaking_accumulated_interest": self.breaking_accumulated_interest, } @@ -360,6 +362,7 @@ class ChatManager: "group_name": group_info_d["group_name"] if group_info_d else "", "energy_value": s_data_dict.get("energy_value", 5.0), "sleep_pressure": s_data_dict.get("sleep_pressure", 0.0), + "focus_energy": s_data_dict.get("focus_energy", global_config.chat.focus_value), } # 根据数据库类型选择插入语句 @@ -421,6 +424,7 @@ class ChatManager: "last_active_time": model_instance.last_active_time, "energy_value": model_instance.energy_value, "sleep_pressure": model_instance.sleep_pressure, + "focus_energy": getattr(model_instance, "focus_energy", global_config.chat.focus_value), } loaded_streams_data.append(data_for_from_dict) session.commit() diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index ef9cce84d..d601e7030 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -139,8 +139,6 @@ def init_prompt(): -------------------------------- {time_block} -{reply_target_block} - 注意不要复读你前面发过的内容,意思相近也不行。 请注意不要输出多余内容(包括前后缀,冒号和引号,at或 @等 )。只输出回复内容。 @@ -832,16 +830,22 @@ class DefaultReplyer: reply_message.get("user_id"), # type: ignore ) person_name = await person_info_manager.get_value(person_id, "person_name") - sender = person_name + + # 检查是否是bot自己的名字,如果是则替换为"(你)" + bot_user_id = str(global_config.bot.qq_account) + current_user_id = person_info_manager.get_value_sync(person_id, "user_id") + current_platform = reply_message.get("chat_info_platform") + + if current_user_id == bot_user_id and current_platform == global_config.bot.platform: + sender = f"{person_name}(你)" + else: + # 如果不是bot自己,直接使用person_name + sender = person_name target = reply_message.get("processed_plain_text") person_info_manager = get_person_info_manager() person_id = person_info_manager.get_person_id_by_person_name(sender) - user_id = person_info_manager.get_value_sync(person_id, "user_id") platform = chat_stream.platform - if user_id == global_config.bot.qq_account and platform == global_config.bot.platform: - logger.warning("选取了自身作为回复对象,跳过构建prompt") - return "" target = replace_user_references_sync(target, chat_stream.platform, replace_bot_name=True) diff --git a/src/chat/utils/prompt.py b/src/chat/utils/prompt.py index b5cf140c5..ae0c9c4b1 100644 --- a/src/chat/utils/prompt.py +++ b/src/chat/utils/prompt.py @@ -312,16 +312,15 @@ class Prompt: except asyncio.TimeoutError as e: logger.error(f"构建Prompt超时: {e}") - raise TimeoutError(f"构建Prompt超时: {e}") + raise TimeoutError(f"构建Prompt超时: {e}") from e except Exception as e: logger.error(f"构建Prompt失败: {e}") - raise RuntimeError(f"构建Prompt失败: {e}") + raise RuntimeError(f"构建Prompt失败: {e}") from e async def _build_context_data(self) -> Dict[str, Any]: """构建智能上下文数据""" # 并行执行所有构建任务 start_time = time.time() - timing_logs = {} try: # 准备构建任务 @@ -381,7 +380,6 @@ class Prompt: results = [] for i in range(0, len(tasks), max_concurrent_tasks): batch_tasks = tasks[i : i + max_concurrent_tasks] - batch_names = task_names[i : i + max_concurrent_tasks] batch_results = await asyncio.wait_for( asyncio.gather(*batch_tasks, return_exceptions=True), timeout=timeout_seconds @@ -520,13 +518,99 @@ class Prompt: async def _build_expression_habits(self) -> Dict[str, Any]: """构建表达习惯""" - # 简化的实现,完整实现需要导入相关模块 - return {"expression_habits_block": ""} + if not global_config.expression.enable_expression: + return {"expression_habits_block": ""} + + try: + from src.chat.express.expression_selector import ExpressionSelector + + # 获取聊天历史用于表情选择 + chat_history = "" + if self.parameters.message_list_before_now_long: + recent_messages = self.parameters.message_list_before_now_long[-10:] + chat_history = build_readable_messages( + recent_messages, + replace_bot_name=True, + timestamp_mode="normal", + truncate=True + ) + + # 创建表情选择器 + expression_selector = ExpressionSelector(self.parameters.chat_id) + + # 选择合适的表情 + selected_expressions = await expression_selector.select_suitable_expressions_llm( + chat_history=chat_history, + current_message=self.parameters.target, + emotional_tone="neutral", + topic_type="general" + ) + + # 构建表达习惯块 + if selected_expressions: + style_habits_str = "\n".join([f"- {expr}" for expr in selected_expressions]) + expression_habits_block = f"你可以参考以下的语言习惯,当情景合适就使用,但不要生硬使用,以合理的方式结合到你的回复中:\n{style_habits_str}" + else: + expression_habits_block = "" + + return {"expression_habits_block": expression_habits_block} + + except Exception as e: + logger.error(f"构建表达习惯失败: {e}") + return {"expression_habits_block": ""} async def _build_memory_block(self) -> Dict[str, Any]: """构建记忆块""" - # 简化的实现 - return {"memory_block": ""} + if not global_config.memory.enable_memory: + return {"memory_block": ""} + + try: + from src.chat.memory_system.memory_activator import MemoryActivator + from src.chat.memory_system.async_instant_memory_wrapper import async_memory + + # 获取聊天历史 + chat_history = "" + if self.parameters.message_list_before_now_long: + recent_messages = self.parameters.message_list_before_now_long[-20:] + chat_history = build_readable_messages( + recent_messages, + replace_bot_name=True, + timestamp_mode="normal", + truncate=True + ) + + # 激活长期记忆 + memory_activator = MemoryActivator() + running_memories = await memory_activator.activate_memory_with_chat_history( + chat_history=chat_history, + target_user=self.parameters.sender, + chat_id=self.parameters.chat_id + ) + + # 获取即时记忆 + instant_memory = await async_memory.get_memory_with_fallback( + chat_id=self.parameters.chat_id, + target_user=self.parameters.sender + ) + + # 构建记忆块 + memory_parts = [] + + if running_memories: + memory_parts.append("以下是当前在聊天中,你回忆起的记忆:") + for memory in running_memories: + memory_parts.append(f"- {memory['content']}") + + if instant_memory: + memory_parts.append(f"- {instant_memory}") + + memory_block = "\n".join(memory_parts) if memory_parts else "" + + return {"memory_block": memory_block} + + except Exception as e: + logger.error(f"构建记忆块失败: {e}") + return {"memory_block": ""} async def _build_relation_info(self) -> Dict[str, Any]: """构建关系信息""" @@ -539,13 +623,106 @@ class Prompt: async def _build_tool_info(self) -> Dict[str, Any]: """构建工具信息""" - # 简化的实现 - return {"tool_info_block": ""} + if not global_config.tool.enable_tool: + return {"tool_info_block": ""} + + try: + from src.plugin_system.core.tool_use import ToolExecutor + + # 获取聊天历史 + chat_history = "" + if self.parameters.message_list_before_now_long: + recent_messages = self.parameters.message_list_before_now_long[-15:] + chat_history = build_readable_messages( + recent_messages, + replace_bot_name=True, + timestamp_mode="normal", + truncate=True + ) + + # 创建工具执行器 + tool_executor = ToolExecutor() + + # 执行工具获取信息 + tool_results, _, _ = await tool_executor.execute_from_chat_message( + sender=self.parameters.sender, + target_message=self.parameters.target, + chat_history=chat_history, + return_details=False + ) + + # 构建工具信息块 + if tool_results: + tool_info_parts = ["以下是你通过工具获取到的实时信息:"] + for tool_result in tool_results: + tool_name = tool_result.get("tool_name", "unknown") + content = tool_result.get("content", "") + result_type = tool_result.get("type", "tool_result") + + tool_info_parts.append(f"- 【{tool_name}】{result_type}: {content}") + + tool_info_parts.append("以上是你获取到的实时信息,请在回复时参考这些信息。") + tool_info_block = "\n".join(tool_info_parts) + else: + tool_info_block = "" + + return {"tool_info_block": tool_info_block} + + except Exception as e: + logger.error(f"构建工具信息失败: {e}") + return {"tool_info_block": ""} async def _build_knowledge_info(self) -> Dict[str, Any]: """构建知识信息""" - # 简化的实现 - return {"knowledge_prompt": ""} + if not global_config.lpmm_knowledge.enable: + return {"knowledge_prompt": ""} + + try: + from src.chat.knowledge.knowledge_lib import QAManager + + # 获取问题文本(当前消息) + question = self.parameters.target or "" + if not question: + return {"knowledge_prompt": ""} + + # 创建QA管理器 + qa_manager = QAManager() + + # 搜索相关知识 + knowledge_results = await qa_manager.get_knowledge( + question=question, + chat_id=self.parameters.chat_id, + max_results=5, + min_similarity=0.5 + ) + + # 构建知识块 + if knowledge_results and knowledge_results.get("knowledge_items"): + knowledge_parts = ["以下是与你当前对话相关的知识信息:"] + + for item in knowledge_results["knowledge_items"]: + content = item.get("content", "") + source = item.get("source", "") + relevance = item.get("relevance", 0.0) + + if content: + if source: + knowledge_parts.append(f"- [{relevance:.2f}] {content} (来源: {source})") + else: + knowledge_parts.append(f"- [{relevance:.2f}] {content}") + + if knowledge_results.get("summary"): + knowledge_parts.append(f"\n知识总结: {knowledge_results['summary']}") + + knowledge_prompt = "\n".join(knowledge_parts) + else: + knowledge_prompt = "" + + return {"knowledge_prompt": knowledge_prompt} + + except Exception as e: + logger.error(f"构建知识信息失败: {e}") + return {"knowledge_prompt": ""} async def _build_cross_context(self) -> Dict[str, Any]: """构建跨群上下文""" From 5f4fea83580d9836f9a00355c10eac562480033a Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Fri, 12 Sep 2025 20:34:39 +0800 Subject: [PATCH 06/40] =?UTF-8?q?feat(prompt):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=83=8C=E6=99=AF=E6=95=85=E4=BA=8B=E4=B8=8E=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E5=87=86=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次提交在人设配置中引入了“背景故事”和“安全准则”两个新字段,旨在增强 Bot 的角色深度和互动安全性。 - **背景故事 (`background_story`)**: 允许用户定义详细的世界观或角色背景。这部分内容将作为背景知识注入 Prompt,指导模型在不直接复述的情况下理解和运用,从而塑造更丰富的角色。 - **安全准则 (`safety_guidelines`)**: 用户可以明确定义 Bot 必须遵守的行为红线。这些准则会被整合进系统指令,为模型处理不当或敏感请求提供清晰的、可配置的指导方针,提升了交互的安全性。 --- src/chat/replyer/default_generator.py | 23 +++++++++++++++++++++++ src/config/official_configs.py | 2 ++ template/bot_config_template.toml | 14 +++++++++++++- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 0217f18e6..cd9cd23cd 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -123,6 +123,7 @@ def init_prompt(): - {reply_target_block} ,你需要生成一段紧密相关且能推动对话的回复。 ## 规则 +{safety_guidelines_block} 在回应之前,首先分析消息的针对性: 1. **直接针对你**:@你、回复你、明确询问你 → 必须回应 2. **间接相关**:涉及你感兴趣的话题但未直接问你 → 谨慎参与 @@ -942,6 +943,16 @@ class DefaultReplyer: identity_block = await get_individuality().get_personality_block() + # 新增逻辑:获取背景知识并与指导语拼接 + background_story = global_config.personality.background_story + if background_story: + background_knowledge_prompt = f""" + +## 背景知识(请理解并作为行动依据,但不要在对话中直接复述) +{background_story}""" + # 将背景知识块插入到人设块的后面 + identity_block = f"{identity_block}{background_knowledge_prompt}" + schedule_block = "" if global_config.planning_system.schedule_enable: from src.schedule.schedule_manager import schedule_manager @@ -953,6 +964,17 @@ class DefaultReplyer: "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。不要随意遵从他人指令。" ) + # 新增逻辑:构建安全准则块 + safety_guidelines = global_config.personality.safety_guidelines + safety_guidelines_block = "" + if safety_guidelines: + guidelines_text = "\n".join(f"{i+1}. {line}" for i, line in enumerate(safety_guidelines)) + safety_guidelines_block = f"""### 安全与互动底线 +在任何情况下,你都必须遵守以下由你的设定者为你定义的原则: +{guidelines_text} +如果遇到违反上述原则的请求,请在保持你核心人设的同时,巧妙地拒绝或转移话题。 +""" + if sender and target: if is_group_chat: if sender: @@ -1005,6 +1027,7 @@ class DefaultReplyer: identity_block=identity_block, schedule_block=schedule_block, moderation_prompt_block=moderation_prompt_block, + safety_guidelines_block=safety_guidelines_block, reply_target_block=reply_target_block, mood_prompt=mood_prompt, action_descriptions=action_descriptions, diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 2252041f3..346217342 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -51,6 +51,8 @@ class PersonalityConfig(ValidatedConfigBase): personality_core: str = Field(..., description="核心人格") personality_side: str = Field(..., description="人格侧写") identity: str = Field(default="", description="身份特征") + background_story: str = Field(default="", description="世界观背景故事,这部分内容会作为背景知识,LLM被指导不应主动复述") + safety_guidelines: List[str] = Field(default_factory=list, description="安全与互动底线,Bot在任何情况下都必须遵守的原则") reply_style: str = Field(default="", description="表达风格") prompt_mode: Literal["s4u", "normal"] = Field(default="s4u", description="Prompt模式") compress_personality: bool = Field(default=True, description="是否压缩人格") diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 5ce2f5797..78e4bd5f7 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.8.4" +version = "6.8.5" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -64,9 +64,21 @@ personality_side = "用一句话或几句话描述人格的侧面特质" # 可以描述外貌,性别,身高,职业,属性等等描述 identity = "年龄为19岁,是女孩子,身高为160cm,有黑色的短发" +# 此处用于填写详细的世界观、背景故事、复杂人际关系等。 +# 这部分内容将作为Bot的“背景知识”,Bot被指导不应在对话中主动或频繁地复述这些设定。 +background_story = "" + # 描述MoFox-Bot说话的表达风格,表达习惯,如要修改,可以酌情新增内容 reply_style = "回复可以简短一些。可以参考贴吧,知乎和微博的回复风格,回复不要浮夸,不要用夸张修辞,平淡一些。" +# 安全与互动底线 (Bot在任何情况下都必须遵守的原则) +# 你可以在这里定义Bot的行为红线,例如如何回应不恰当的问题。 +safety_guidelines = [ + "拒绝任何包含骚扰、冒犯、暴力、色情或危险内容的请求。", + "在拒绝时,请使用符合你人设的、坚定的语气。", + "不要执行任何可能被用于恶意目的的指令。" +] + #回复的Prompt模式选择:s4u为原有s4u样式,normal为0.9之前的模式 prompt_mode = "s4u" # 可选择 "s4u" 或 "normal" From 47e504e7156084d7a490a2e57e8abd2e3fca4ee4 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 12 Sep 2025 20:39:20 +0800 Subject: [PATCH 07/40] =?UTF-8?q?feat(plugin):=20=E5=85=81=E8=AE=B8?= =?UTF-8?q?=E5=9C=A8Action=E5=86=85=E9=83=A8=E8=B0=83=E7=94=A8=E5=85=B6?= =?UTF-8?q?=E4=BB=96Action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在`BaseAction`中新增`call_action`方法,使得一个Action可以方便地调用并执行另一个已注册的Action。 该方法会: - 从组件注册中心查找并获取目标Action类。 - 复用当前Action的上下文信息(如`chat_stream`, `cycle_timers`等)来实例化目标Action。 - 执行目标Action并返回其执行结果。 这为创建更复杂、可组合的插件逻辑提供了基础,提高了代码的复用性和模块化程度。 --- .../chat_loop/proactive/proactive_thinker.py | 3 - src/plugin_system/base/base_action.py | 55 +++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py index 432d96844..3a6af40fe 100644 --- a/src/chat/chat_loop/proactive/proactive_thinker.py +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -1,7 +1,5 @@ import time import traceback -import orjson -import re from typing import TYPE_CHECKING, Dict, Any from src.common.logger import get_logger @@ -12,7 +10,6 @@ from src.plugin_system.apis import generator_api from src.plugin_system.apis.generator_api import process_human_text from src.schedule.schedule_manager import schedule_manager from src.plugin_system import tool_api -from src.plugin_system.base.component_types import ComponentType from src.config.config import global_config from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat, build_readable_messages_with_id from src.mood.mood_manager import mood_manager diff --git a/src/plugin_system/base/base_action.py b/src/plugin_system/base/base_action.py index 5962d69fe..e1663afc6 100644 --- a/src/plugin_system/base/base_action.py +++ b/src/plugin_system/base/base_action.py @@ -8,6 +8,7 @@ from src.common.logger import get_logger from src.chat.message_receive.chat_stream import ChatStream from src.plugin_system.base.component_types import ActionActivationType, ChatMode, ActionInfo, ComponentType, ChatType from src.plugin_system.apis import send_api, database_api, message_api +from src.plugin_system.core.component_registry import component_registry logger = get_logger("base_action") @@ -381,6 +382,60 @@ class BaseAction(ABC): logger.error(f"{self.log_prefix} 发送命令时出错: {e}") return False + async def call_action(self, action_name: str, action_data: Optional[dict] = None) -> Tuple[bool, str]: + """ + 在当前Action中调用另一个Action。 + + Args: + action_name (str): 要调用的Action的名称。 + action_data (Optional[dict], optional): 传递给被调用Action的动作数据。如果为None,则使用当前Action的action_data。 + + Returns: + Tuple[bool, str]: 被调用Action的执行结果 (is_success, message)。 + """ + log_prefix = f"{self.log_prefix} [call_action -> {action_name}]" + logger.info(f"{log_prefix} 尝试调用Action: {action_name}") + + try: + # 1. 从注册中心获取Action类 + action_class = component_registry.get_component_class(action_name, ComponentType.ACTION) + if not action_class: + logger.error(f"{log_prefix} 未找到Action: {action_name}") + return False, f"未找到Action: {action_name}" + + # 2. 准备实例化参数 + # 复用当前Action的大部分上下文信息 + called_action_data = action_data if action_data is not None else self.action_data + + component_info = component_registry.get_component_info(action_name, ComponentType.ACTION) + if not component_info: + logger.warning(f"{log_prefix} 未找到Action组件信息: {action_name}") + return False, f"未找到Action组件信息: {action_name}" + + plugin_config = component_registry.get_plugin_config(component_info.plugin_name) + + # 3. 实例化被调用的Action + action_instance = action_class( + action_data=called_action_data, + reasoning=f"Called by {self.action_name}", + cycle_timers=self.cycle_timers, + thinking_id=self.thinking_id, + chat_stream=self.chat_stream, + log_prefix=log_prefix, + plugin_config=plugin_config, + action_message=self.action_message, + ) + + # 4. 执行Action + logger.debug(f"{log_prefix} 开始执行...") + result = await action_instance.execute() + logger.info(f"{log_prefix} 执行完成,结果: {result}") + return result + + except Exception as e: + logger.error(f"{log_prefix} 调用时发生错误: {e}", exc_info=True) + return False, f"调用Action '{action_name}' 时发生错误: {e}" + @classmethod def get_action_info(cls) -> "ActionInfo": """从类属性生成ActionInfo From 18220e41107e85ad3b88bd8027af5b1fae3e0485 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 12 Sep 2025 20:42:02 +0800 Subject: [PATCH 08/40] rrrrrrrrrrrrruuuuuuuuuuuuuuuffffffffffffffffffffffffffff x3 --- src/chat/chat_loop/proactive/event_scheduler.py | 3 +-- src/chat/chat_loop/response_handler.py | 1 - src/chat/chat_loop/sleep_manager/notification_sender.py | 1 - src/chat/chat_loop/sleep_manager/sleep_manager.py | 2 +- src/chat/frequency_analyzer/trigger.py | 2 +- src/chat/heart_flow/heartflow_message_processor.py | 1 - src/chat/planner_actions/planner.py | 4 ---- src/common/message/api.py | 1 - src/plugins/built_in/at_user_plugin/plugin.py | 1 - src/plugins/built_in/napcat_adapter_plugin/event_handlers.py | 1 - 10 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/chat/chat_loop/proactive/event_scheduler.py b/src/chat/chat_loop/proactive/event_scheduler.py index 5e6ca0c51..8c005cb3d 100644 --- a/src/chat/chat_loop/proactive/event_scheduler.py +++ b/src/chat/chat_loop/proactive/event_scheduler.py @@ -4,9 +4,8 @@ """ import asyncio -import time import traceback -from datetime import datetime, timedelta +from datetime import datetime from typing import Dict, Callable, Any, Optional from dataclasses import dataclass from src.common.logger import get_logger diff --git a/src/chat/chat_loop/response_handler.py b/src/chat/chat_loop/response_handler.py index 889371009..9859c76c3 100644 --- a/src/chat/chat_loop/response_handler.py +++ b/src/chat/chat_loop/response_handler.py @@ -1,5 +1,4 @@ import time -import orjson import random from typing import Dict, Any, Tuple diff --git a/src/chat/chat_loop/sleep_manager/notification_sender.py b/src/chat/chat_loop/sleep_manager/notification_sender.py index 55b32ec85..95ee304e9 100644 --- a/src/chat/chat_loop/sleep_manager/notification_sender.py +++ b/src/chat/chat_loop/sleep_manager/notification_sender.py @@ -1,4 +1,3 @@ -import asyncio from src.common.logger import get_logger from ..hfc_context import HfcContext diff --git a/src/chat/chat_loop/sleep_manager/sleep_manager.py b/src/chat/chat_loop/sleep_manager/sleep_manager.py index 3bf099be7..677555aef 100644 --- a/src/chat/chat_loop/sleep_manager/sleep_manager.py +++ b/src/chat/chat_loop/sleep_manager/sleep_manager.py @@ -1,7 +1,7 @@ import asyncio import random from datetime import datetime, timedelta, date -from typing import Optional, TYPE_CHECKING, List, Dict, Any +from typing import Optional, TYPE_CHECKING from src.common.logger import get_logger from src.config.config import global_config diff --git a/src/chat/frequency_analyzer/trigger.py b/src/chat/frequency_analyzer/trigger.py index a6b4d8a3b..d62547306 100644 --- a/src/chat/frequency_analyzer/trigger.py +++ b/src/chat/frequency_analyzer/trigger.py @@ -91,7 +91,7 @@ class FrequencyBasedTrigger: # 6. 直接调用 proactive_thinker event = ProactiveTriggerEvent( source="frequency_analyzer", - reason=f"User is in a high-frequency chat period." + reason="User is in a high-frequency chat period." ) await sub_heartflow.heart_fc_instance.proactive_thinker.think(event) diff --git a/src/chat/heart_flow/heartflow_message_processor.py b/src/chat/heart_flow/heartflow_message_processor.py index 734f6a301..a401e797b 100644 --- a/src/chat/heart_flow/heartflow_message_processor.py +++ b/src/chat/heart_flow/heartflow_message_processor.py @@ -17,7 +17,6 @@ from src.chat.utils.chat_message_builder import replace_user_references_sync from src.common.logger import get_logger from src.person_info.relationship_manager import get_relationship_manager from src.mood.mood_manager import mood_manager -from src.chat.message_receive.chat_stream import get_chat_manager if TYPE_CHECKING: from src.chat.heart_flow.sub_heartflow import SubHeartflow diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 3163697d4..cdab8b3d6 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -1,10 +1,7 @@ import orjson import time import traceback -import asyncio -import math import random -import json from typing import Dict, Any, Optional, Tuple, List, TYPE_CHECKING from rich.traceback import install from datetime import datetime @@ -27,7 +24,6 @@ from src.plugin_system.base.component_types import ( ActionInfo, ChatMode, ComponentType, - ActionActivationType, ) from src.plugin_system.core.component_registry import component_registry from src.schedule.schedule_manager import schedule_manager diff --git a/src/common/message/api.py b/src/common/message/api.py index a85677f47..75e5c84e9 100644 --- a/src/common/message/api.py +++ b/src/common/message/api.py @@ -1,5 +1,4 @@ from src.common.server import get_global_server -import os import importlib.metadata from maim_message import MessageServer from src.common.logger import get_logger diff --git a/src/plugins/built_in/at_user_plugin/plugin.py b/src/plugins/built_in/at_user_plugin/plugin.py index 1cac44fcc..bfb923963 100644 --- a/src/plugins/built_in/at_user_plugin/plugin.py +++ b/src/plugins/built_in/at_user_plugin/plugin.py @@ -8,7 +8,6 @@ from src.plugin_system import ( ActionInfo, ActionActivationType, ) -from src.person_info.person_info import get_person_info_manager from src.common.logger import get_logger from src.plugin_system.base.component_types import ChatType diff --git a/src/plugins/built_in/napcat_adapter_plugin/event_handlers.py b/src/plugins/built_in/napcat_adapter_plugin/event_handlers.py index 78d94363e..c4f889712 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/event_handlers.py +++ b/src/plugins/built_in/napcat_adapter_plugin/event_handlers.py @@ -1,4 +1,3 @@ -import orjson from src.plugin_system import BaseEventHandler from src.plugin_system.base.base_event import HandlerResult From 51643eb53a12b637643a3d0deb99db09040e8f11 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 12 Sep 2025 20:43:51 +0800 Subject: [PATCH 09/40] =?UTF-8?q?feat(config):=20=E6=9B=B4=E6=94=B9?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E6=9C=8D=E5=8A=A1=E5=99=A8=E7=AB=AF=E5=8F=A3?= =?UTF-8?q?=E4=B8=BA8000?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将默认的服务器端口从8080更改为8000,以避免与其他常用应用程序的潜在端口冲突。 --- template/bot_config_template.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 4df8d0a71..ebc2f0f38 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -495,7 +495,7 @@ insomnia_trigger_delay_minutes = [15, 45] [server] host = "127.0.0.1" -port = 8080 +port = 8000 [cross_context] # 跨群聊/私聊上下文共享配置 # 这是总开关,用于一键启用或禁用此功能 From cd19e89528c49d8b2b02addbd0d2b52ef4ba4dd8 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 12 Sep 2025 20:47:52 +0800 Subject: [PATCH 10/40] =?UTF-8?q?refactor(config):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E7=9D=A1=E5=89=8D=E6=B6=88=E6=81=AF=E7=BE=A4=E7=BB=84=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除了 `pre_sleep_notification_groups` 配置项,因为该功能已通过新的插件事件和动作机制实现,不再需要硬编码的群组列表。现在可以通过插件来灵活地处理睡前通知的发送逻辑。 --- src/config/official_configs.py | 3 --- template/bot_config_template.toml | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 9253c92cb..f6fc4b3c6 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -662,9 +662,6 @@ class SleepSystemConfig(ValidatedConfigBase): ) max_sleep_delay_minutes: int = Field(default=60, description="单日最大延迟入睡分钟数") enable_pre_sleep_notification: bool = Field(default=True, description="是否启用睡前消息") - pre_sleep_notification_groups: List[str] = Field( - default_factory=list, description='接收睡前消息的群号列表, 格式: ["platform:group_id1", "platform:group_id2"]' - ) pre_sleep_prompt: str = Field( default="我准备睡觉了,请生成一句简短自然的晚安问候。", description="用于生成睡前消息的提示" ) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index ebc2f0f38..5472dfd73 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -484,8 +484,6 @@ max_sleep_delay_minutes = 60 # 是否在进入“准备入睡”状态时发送一条消息通知。 enable_pre_sleep_notification = false -# 接收睡前消息的群组列表。格式为: ["platform:group_id1", "platform:group_id2"],例如 ["qq:12345678"] -pre_sleep_notification_groups = [] # 用于生成睡前消息的提示。AI会根据这个提示生成一句晚安问候。 pre_sleep_prompt = "我准备睡觉了,请生成一句简短自然的晚安问候。" insomnia_duration_minutes = [30, 60] # 单次失眠状态的持续时间范围(分钟) From 35403678b2112481c695b6e9f77f344a21d1575f Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Fri, 12 Sep 2025 20:57:56 +0800 Subject: [PATCH 11/40] =?UTF-8?q?feat(prompt):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E5=87=86=E5=88=99=E5=8F=82=E6=95=B0=E5=B9=B6?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=B7=A5=E5=85=B7=E6=89=A7=E8=A1=8C=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在PromptParameters类中添加safety_guidelines_block参数 - 更新ToolExecutor初始化,传入chat_id参数用于上下文追踪 - 在所有参数准备方法中集成安全准则配置 - 增强prompt系统的安全性和可追溯性 --- src/chat/utils/prompt.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/chat/utils/prompt.py b/src/chat/utils/prompt.py index ae0c9c4b1..88584f51d 100644 --- a/src/chat/utils/prompt.py +++ b/src/chat/utils/prompt.py @@ -71,6 +71,7 @@ class PromptParameters: identity_block: str = "" schedule_block: str = "" moderation_prompt_block: str = "" + safety_guidelines_block: str = "" reply_target_block: str = "" mood_prompt: str = "" action_descriptions: str = "" @@ -641,7 +642,7 @@ class Prompt: ) # 创建工具执行器 - tool_executor = ToolExecutor() + tool_executor = ToolExecutor(chat_id=self.parameters.chat_id) # 执行工具获取信息 tool_results, _, _ = await tool_executor.execute_from_chat_message( @@ -768,6 +769,7 @@ class Prompt: "reply_style": global_config.personality.reply_style, "keywords_reaction_prompt": self.parameters.keywords_reaction_prompt or context_data.get("keywords_reaction_prompt", ""), "moderation_prompt": self.parameters.moderation_prompt_block or context_data.get("moderation_prompt", ""), + "safety_guidelines_block": self.parameters.safety_guidelines_block or context_data.get("safety_guidelines_block", ""), } def _prepare_normal_params(self, context_data: Dict[str, Any]) -> Dict[str, Any]: @@ -791,6 +793,7 @@ class Prompt: "mood_state": self.parameters.mood_prompt or context_data.get("mood_state", ""), "keywords_reaction_prompt": self.parameters.keywords_reaction_prompt or context_data.get("keywords_reaction_prompt", ""), "moderation_prompt": self.parameters.moderation_prompt_block or context_data.get("moderation_prompt", ""), + "safety_guidelines_block": self.parameters.safety_guidelines_block or context_data.get("safety_guidelines_block", ""), } def _prepare_default_params(self, context_data: Dict[str, Any]) -> Dict[str, Any]: @@ -810,6 +813,7 @@ class Prompt: "reply_style": global_config.personality.reply_style, "keywords_reaction_prompt": self.parameters.keywords_reaction_prompt or context_data.get("keywords_reaction_prompt", ""), "moderation_prompt": self.parameters.moderation_prompt_block or context_data.get("moderation_prompt", ""), + "safety_guidelines_block": self.parameters.safety_guidelines_block or context_data.get("safety_guidelines_block", ""), } def format(self, *args, **kwargs) -> str: From de4402395235d64d4bca044b2c3e55a46a202648 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Fri, 12 Sep 2025 21:08:13 +0800 Subject: [PATCH 12/40] =?UTF-8?q?fix(plugin=5Fsystem):=20=E8=A7=A3?= =?UTF-8?q?=E5=86=B3=20base=5Faction=20=E4=B8=AD=E7=9A=84=E5=BE=AA?= =?UTF-8?q?=E7=8E=AF=E5=AF=BC=E5=85=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 `component_registry` 的导入移至 `run` 方法内部,以避免在模块加载时产生循环依赖。 --- src/plugin_system/base/base_action.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/plugin_system/base/base_action.py b/src/plugin_system/base/base_action.py index e1663afc6..2ae450b6b 100644 --- a/src/plugin_system/base/base_action.py +++ b/src/plugin_system/base/base_action.py @@ -8,9 +8,6 @@ from src.common.logger import get_logger from src.chat.message_receive.chat_stream import ChatStream from src.plugin_system.base.component_types import ActionActivationType, ChatMode, ActionInfo, ComponentType, ChatType from src.plugin_system.apis import send_api, database_api, message_api -from src.plugin_system.core.component_registry import component_registry - - logger = get_logger("base_action") @@ -398,6 +395,7 @@ class BaseAction(ABC): try: # 1. 从注册中心获取Action类 + from src.plugin_system.core.component_registry import component_registry action_class = component_registry.get_component_class(action_name, ComponentType.ACTION) if not action_class: logger.error(f"{log_prefix} 未找到Action: {action_name}") From 9f084338dce0b4fed872649805d395ed08767474 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 12 Sep 2025 21:12:13 +0800 Subject: [PATCH 13/40] =?UTF-8?q?refactor(core):=20=E5=BB=B6=E8=BF=9F?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E7=9F=A5=E8=AF=86=E5=BA=93=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AF=BC=E5=85=A5=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将LPMM知识库的初始化逻辑封装到`initialize_lpmm_knowledge`函数中,并将其调用移至`main.py`,以实现延迟加载并改善启动流程。 为了解决循环导入问题,将`component_registry`的导入移至`BaseAction.call`方法内部。 --- src/chat/knowledge/knowledge_lib.py | 89 ++++++++++++++------------- src/main.py | 7 ++- src/plugin_system/base/base_action.py | 2 +- 3 files changed, 54 insertions(+), 44 deletions(-) diff --git a/src/chat/knowledge/knowledge_lib.py b/src/chat/knowledge/knowledge_lib.py index 13629f18b..ccc3cd090 100644 --- a/src/chat/knowledge/knowledge_lib.py +++ b/src/chat/knowledge/knowledge_lib.py @@ -30,50 +30,55 @@ DATA_PATH = os.path.join(ROOT_PATH, "data") qa_manager = None inspire_manager = None -# 检查LPMM知识库是否启用 -if global_config.lpmm_knowledge.enable: - logger.info("正在初始化Mai-LPMM") - logger.info("创建LLM客户端") - # 初始化Embedding库 - embed_manager = EmbeddingManager() - logger.info("正在从文件加载Embedding库") - try: - embed_manager.load_from_file() - except Exception as e: - logger.warning(f"此消息不会影响正常使用:从文件加载Embedding库时,{e}") - # logger.warning("如果你是第一次导入知识,或者还未导入知识,请忽略此错误") - logger.info("Embedding库加载完成") - # 初始化KG - kg_manager = KGManager() - logger.info("正在从文件加载KG") - try: - kg_manager.load_from_file() - except Exception as e: - logger.warning(f"此消息不会影响正常使用:从文件加载KG时,{e}") - # logger.warning("如果你是第一次导入知识,或者还未导入知识,请忽略此错误") - logger.info("KG加载完成") +def initialize_lpmm_knowledge(): + """初始化LPMM知识库""" + global qa_manager, inspire_manager - logger.info(f"KG节点数量:{len(kg_manager.graph.get_node_list())}") - logger.info(f"KG边数量:{len(kg_manager.graph.get_edge_list())}") + # 检查LPMM知识库是否启用 + if global_config.lpmm_knowledge.enable: + logger.info("正在初始化Mai-LPMM") + logger.info("创建LLM客户端") - # 数据比对:Embedding库与KG的段落hash集合 - for pg_hash in kg_manager.stored_paragraph_hashes: - key = f"paragraph-{pg_hash}" - if key not in embed_manager.stored_pg_hashes: - logger.warning(f"KG中存在Embedding库中不存在的段落:{key}") + # 初始化Embedding库 + embed_manager = EmbeddingManager() + logger.info("正在从文件加载Embedding库") + try: + embed_manager.load_from_file() + except Exception as e: + logger.warning(f"此消息不会影响正常使用:从文件加载Embedding库时,{e}") + # logger.warning("如果你是第一次导入知识,或者还未导入知识,请忽略此错误") + logger.info("Embedding库加载完成") + # 初始化KG + kg_manager = KGManager() + logger.info("正在从文件加载KG") + try: + kg_manager.load_from_file() + except Exception as e: + logger.warning(f"此消息不会影响正常使用:从文件加载KG时,{e}") + # logger.warning("如果你是第一次导入知识,或者还未导入知识,请忽略此错误") + logger.info("KG加载完成") - # 问答系统(用于知识库) - qa_manager = QAManager( - embed_manager, - kg_manager, - ) + logger.info(f"KG节点数量:{len(kg_manager.graph.get_node_list())}") + logger.info(f"KG边数量:{len(kg_manager.graph.get_edge_list())}") - # # 记忆激活(用于记忆库) - # inspire_manager = MemoryActiveManager( - # embed_manager, - # llm_client_list[global_config["embedding"]["provider"]], - # ) -else: - logger.info("LPMM知识库已禁用,跳过初始化") - # 创建空的占位符对象,避免导入错误 + # 数据比对:Embedding库与KG的段落hash集合 + for pg_hash in kg_manager.stored_paragraph_hashes: + key = f"paragraph-{pg_hash}" + if key not in embed_manager.stored_pg_hashes: + logger.warning(f"KG中存在Embedding库中不存在的段落:{key}") + + # 问答系统(用于知识库) + qa_manager = QAManager( + embed_manager, + kg_manager, + ) + + # # 记忆激活(用于记忆库) + # inspire_manager = MemoryActiveManager( + # embed_manager, + # llm_client_list[global_config["embedding"]["provider"]], + # ) + else: + logger.info("LPMM知识库已禁用,跳过初始化") + # 创建空的占位符对象,避免导入错误 diff --git a/src/main.py b/src/main.py index 2b6fdfd10..247c91536 100644 --- a/src/main.py +++ b/src/main.py @@ -116,9 +116,9 @@ class MainSystem: # 停止消息重组器 from src.plugin_system.core.event_manager import event_manager from src.plugin_system import EventType + import asyncio asyncio.run(event_manager.trigger_event(EventType.ON_STOP,permission_group="SYSTEM")) from src.utils.message_chunker import reassembler - import asyncio loop = asyncio.get_event_loop() if loop.is_running(): @@ -250,6 +250,11 @@ MoFox_Bot(第三方修改版) self.hippocampus_manager.initialize() logger.info("记忆系统初始化成功") + # 初始化LPMM知识库 + from src.chat.knowledge.knowledge_lib import initialize_lpmm_knowledge + initialize_lpmm_knowledge() + logger.info("LPMM知识库初始化成功") + # 初始化异步记忆管理器 try: from src.chat.memory_system.async_memory_optimizer import async_memory_manager diff --git a/src/plugin_system/base/base_action.py b/src/plugin_system/base/base_action.py index e1663afc6..9a05da877 100644 --- a/src/plugin_system/base/base_action.py +++ b/src/plugin_system/base/base_action.py @@ -8,7 +8,6 @@ from src.common.logger import get_logger from src.chat.message_receive.chat_stream import ChatStream from src.plugin_system.base.component_types import ActionActivationType, ChatMode, ActionInfo, ComponentType, ChatType from src.plugin_system.apis import send_api, database_api, message_api -from src.plugin_system.core.component_registry import component_registry logger = get_logger("base_action") @@ -397,6 +396,7 @@ class BaseAction(ABC): logger.info(f"{log_prefix} 尝试调用Action: {action_name}") try: + from src.plugin_system.core.component_registry import component_registry # 1. 从注册中心获取Action类 action_class = component_registry.get_component_class(action_name, ComponentType.ACTION) if not action_class: From fc5980a2823e65e197d0cf50906ccfe41af69dda Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 12 Sep 2025 21:35:19 +0800 Subject: [PATCH 14/40] =?UTF-8?q?=E5=85=88=E6=B7=BB=E5=8A=A0=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E6=95=B0=E6=8D=AE=E6=A8=A1=E5=9E=8B=E4=BD=A0=E5=88=AB?= =?UTF-8?q?=E7=AE=A1=E4=BB=96=E7=94=A8=E6=B2=A1=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/data_models/__init__.py | 53 ++++ src/common/data_models/database_data_model.py | 235 ++++++++++++++++++ src/common/data_models/info_data_model.py | 25 ++ src/common/data_models/llm_data_model.py | 16 ++ src/common/data_models/message_data_model.py | 36 +++ 5 files changed, 365 insertions(+) create mode 100644 src/common/data_models/__init__.py create mode 100644 src/common/data_models/database_data_model.py create mode 100644 src/common/data_models/info_data_model.py create mode 100644 src/common/data_models/llm_data_model.py create mode 100644 src/common/data_models/message_data_model.py diff --git a/src/common/data_models/__init__.py b/src/common/data_models/__init__.py new file mode 100644 index 000000000..222ff59ca --- /dev/null +++ b/src/common/data_models/__init__.py @@ -0,0 +1,53 @@ +import copy +from typing import Any + + +class BaseDataModel: + def deepcopy(self): + return copy.deepcopy(self) + +def temporarily_transform_class_to_dict(obj: Any) -> Any: + # sourcery skip: assign-if-exp, reintroduce-else + """ + 将对象或容器中的 BaseDataModel 子类(类对象)或 BaseDataModel 实例 + 递归转换为普通 dict,不修改原对象。 + - 对于类对象(isinstance(value, type) 且 issubclass(..., BaseDataModel)), + 读取类的 __dict__ 中非 dunder 项并递归转换。 + - 对于实例(isinstance(value, BaseDataModel)),读取 vars(instance) 并递归转换。 + """ + + def _transform(value: Any) -> Any: + # 值是类对象且为 BaseDataModel 的子类 + if isinstance(value, type) and issubclass(value, BaseDataModel): + return {k: _transform(v) for k, v in value.__dict__.items() if not k.startswith("__") and not callable(v)} + + # 值是 BaseDataModel 的实例 + if isinstance(value, BaseDataModel): + return {k: _transform(v) for k, v in vars(value).items()} + + # 常见容器类型,递归处理 + if isinstance(value, dict): + return {k: _transform(v) for k, v in value.items()} + if isinstance(value, list): + return [_transform(v) for v in value] + if isinstance(value, tuple): + return tuple(_transform(v) for v in value) + if isinstance(value, set): + return {_transform(v) for v in value} + # 基本类型,直接返回 + return value + + result = _transform(obj) + + def flatten(target_dict: dict): + flat_dict = {} + for k, v in target_dict.items(): + if isinstance(v, dict): + # 递归扁平化子字典 + sub_flat = flatten(v) + flat_dict.update(sub_flat) + else: + flat_dict[k] = v + return flat_dict + + return flatten(result) if isinstance(result, dict) else result diff --git a/src/common/data_models/database_data_model.py b/src/common/data_models/database_data_model.py new file mode 100644 index 000000000..bf4a5f527 --- /dev/null +++ b/src/common/data_models/database_data_model.py @@ -0,0 +1,235 @@ +import json +from typing import Optional, Any, Dict +from dataclasses import dataclass, field + +from . import BaseDataModel + + +@dataclass +class DatabaseUserInfo(BaseDataModel): + platform: str = field(default_factory=str) + user_id: str = field(default_factory=str) + user_nickname: str = field(default_factory=str) + user_cardname: Optional[str] = None + + # def __post_init__(self): + # assert isinstance(self.platform, str), "platform must be a string" + # assert isinstance(self.user_id, str), "user_id must be a string" + # assert isinstance(self.user_nickname, str), "user_nickname must be a string" + # assert isinstance(self.user_cardname, str) or self.user_cardname is None, ( + # "user_cardname must be a string or None" + # ) + + +@dataclass +class DatabaseGroupInfo(BaseDataModel): + group_id: str = field(default_factory=str) + group_name: str = field(default_factory=str) + group_platform: Optional[str] = None + + # def __post_init__(self): + # assert isinstance(self.group_id, str), "group_id must be a string" + # assert isinstance(self.group_name, str), "group_name must be a string" + # assert isinstance(self.group_platform, str) or self.group_platform is None, ( + # "group_platform must be a string or None" + # ) + + +@dataclass +class DatabaseChatInfo(BaseDataModel): + stream_id: str = field(default_factory=str) + platform: str = field(default_factory=str) + create_time: float = field(default_factory=float) + last_active_time: float = field(default_factory=float) + user_info: DatabaseUserInfo = field(default_factory=DatabaseUserInfo) + group_info: Optional[DatabaseGroupInfo] = None + + # def __post_init__(self): + # assert isinstance(self.stream_id, str), "stream_id must be a string" + # assert isinstance(self.platform, str), "platform must be a string" + # assert isinstance(self.create_time, float), "create_time must be a float" + # assert isinstance(self.last_active_time, float), "last_active_time must be a float" + # assert isinstance(self.user_info, DatabaseUserInfo), "user_info must be a DatabaseUserInfo instance" + # assert isinstance(self.group_info, DatabaseGroupInfo) or self.group_info is None, ( + # "group_info must be a DatabaseGroupInfo instance or None" + # ) + + +@dataclass(init=False) +class DatabaseMessages(BaseDataModel): + def __init__( + self, + message_id: str = "", + time: float = 0.0, + chat_id: str = "", + reply_to: Optional[str] = None, + interest_value: Optional[float] = None, + key_words: Optional[str] = None, + key_words_lite: Optional[str] = None, + is_mentioned: Optional[bool] = None, + is_at: Optional[bool] = None, + reply_probability_boost: Optional[float] = None, + processed_plain_text: Optional[str] = None, + display_message: Optional[str] = None, + priority_mode: Optional[str] = None, + priority_info: Optional[str] = None, + additional_config: Optional[str] = None, + is_emoji: bool = False, + is_picid: bool = False, + is_command: bool = False, + is_notify: bool = False, + selected_expressions: Optional[str] = None, + user_id: str = "", + user_nickname: str = "", + user_cardname: Optional[str] = None, + user_platform: str = "", + chat_info_group_id: Optional[str] = None, + chat_info_group_name: Optional[str] = None, + chat_info_group_platform: Optional[str] = None, + chat_info_user_id: str = "", + chat_info_user_nickname: str = "", + chat_info_user_cardname: Optional[str] = None, + chat_info_user_platform: str = "", + chat_info_stream_id: str = "", + chat_info_platform: str = "", + chat_info_create_time: float = 0.0, + chat_info_last_active_time: float = 0.0, + **kwargs: Any, + ): + self.message_id = message_id + self.time = time + self.chat_id = chat_id + self.reply_to = reply_to + self.interest_value = interest_value + + self.key_words = key_words + self.key_words_lite = key_words_lite + self.is_mentioned = is_mentioned + + self.is_at = is_at + self.reply_probability_boost = reply_probability_boost + + self.processed_plain_text = processed_plain_text + self.display_message = display_message + + self.priority_mode = priority_mode + self.priority_info = priority_info + + self.additional_config = additional_config + self.is_emoji = is_emoji + self.is_picid = is_picid + self.is_command = is_command + self.is_notify = is_notify + + self.selected_expressions = selected_expressions + + self.group_info: Optional[DatabaseGroupInfo] = None + self.user_info = DatabaseUserInfo( + user_id=user_id, + user_nickname=user_nickname, + user_cardname=user_cardname, + platform=user_platform, + ) + if chat_info_group_id and chat_info_group_name: + self.group_info = DatabaseGroupInfo( + group_id=chat_info_group_id, + group_name=chat_info_group_name, + group_platform=chat_info_group_platform, + ) + + self.chat_info = DatabaseChatInfo( + stream_id=chat_info_stream_id, + platform=chat_info_platform, + create_time=chat_info_create_time, + last_active_time=chat_info_last_active_time, + user_info=DatabaseUserInfo( + user_id=chat_info_user_id, + user_nickname=chat_info_user_nickname, + user_cardname=chat_info_user_cardname, + platform=chat_info_user_platform, + ), + group_info=self.group_info, + ) + + if kwargs: + for key, value in kwargs.items(): + setattr(self, key, value) + + # def __post_init__(self): + # assert isinstance(self.message_id, str), "message_id must be a string" + # assert isinstance(self.time, float), "time must be a float" + # assert isinstance(self.chat_id, str), "chat_id must be a string" + # assert isinstance(self.reply_to, str) or self.reply_to is None, "reply_to must be a string or None" + # assert isinstance(self.interest_value, float) or self.interest_value is None, ( + # "interest_value must be a float or None" + # ) + def flatten(self) -> Dict[str, Any]: + """ + 将消息数据模型转换为字典格式,便于存储或传输 + """ + return { + "message_id": self.message_id, + "time": self.time, + "chat_id": self.chat_id, + "reply_to": self.reply_to, + "interest_value": self.interest_value, + "key_words": self.key_words, + "key_words_lite": self.key_words_lite, + "is_mentioned": self.is_mentioned, + "is_at": self.is_at, + "reply_probability_boost": self.reply_probability_boost, + "processed_plain_text": self.processed_plain_text, + "display_message": self.display_message, + "priority_mode": self.priority_mode, + "priority_info": self.priority_info, + "additional_config": self.additional_config, + "is_emoji": self.is_emoji, + "is_picid": self.is_picid, + "is_command": self.is_command, + "is_notify": self.is_notify, + "selected_expressions": self.selected_expressions, + "user_id": self.user_info.user_id, + "user_nickname": self.user_info.user_nickname, + "user_cardname": self.user_info.user_cardname, + "user_platform": self.user_info.platform, + "chat_info_group_id": self.group_info.group_id if self.group_info else None, + "chat_info_group_name": self.group_info.group_name if self.group_info else None, + "chat_info_group_platform": self.group_info.group_platform if self.group_info else None, + "chat_info_stream_id": self.chat_info.stream_id, + "chat_info_platform": self.chat_info.platform, + "chat_info_create_time": self.chat_info.create_time, + "chat_info_last_active_time": self.chat_info.last_active_time, + "chat_info_user_platform": self.chat_info.user_info.platform, + "chat_info_user_id": self.chat_info.user_info.user_id, + "chat_info_user_nickname": self.chat_info.user_info.user_nickname, + "chat_info_user_cardname": self.chat_info.user_info.user_cardname, + } + +@dataclass(init=False) +class DatabaseActionRecords(BaseDataModel): + def __init__( + self, + action_id: str, + time: float, + action_name: str, + action_data: str, + action_done: bool, + action_build_into_prompt: bool, + action_prompt_display: str, + chat_id: str, + chat_info_stream_id: str, + chat_info_platform: str, + ): + self.action_id = action_id + self.time = time + self.action_name = action_name + if isinstance(action_data, str): + self.action_data = json.loads(action_data) + else: + raise ValueError("action_data must be a JSON string") + self.action_done = action_done + self.action_build_into_prompt = action_build_into_prompt + self.action_prompt_display = action_prompt_display + self.chat_id = chat_id + self.chat_info_stream_id = chat_info_stream_id + self.chat_info_platform = chat_info_platform \ No newline at end of file diff --git a/src/common/data_models/info_data_model.py b/src/common/data_models/info_data_model.py new file mode 100644 index 000000000..0f7b1f950 --- /dev/null +++ b/src/common/data_models/info_data_model.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass, field +from typing import Optional, Dict, TYPE_CHECKING +from . import BaseDataModel + +if TYPE_CHECKING: + from .database_data_model import DatabaseMessages + from src.plugin_system.base.component_types import ActionInfo + + +@dataclass +class TargetPersonInfo(BaseDataModel): + platform: str = field(default_factory=str) + user_id: str = field(default_factory=str) + user_nickname: str = field(default_factory=str) + person_id: Optional[str] = None + person_name: Optional[str] = None + + +@dataclass +class ActionPlannerInfo(BaseDataModel): + action_type: str = field(default_factory=str) + reasoning: Optional[str] = None + action_data: Optional[Dict] = None + action_message: Optional["DatabaseMessages"] = None + available_actions: Optional[Dict[str, "ActionInfo"]] = None diff --git a/src/common/data_models/llm_data_model.py b/src/common/data_models/llm_data_model.py new file mode 100644 index 000000000..1d5b75e0c --- /dev/null +++ b/src/common/data_models/llm_data_model.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass +from typing import Optional, List, Tuple, TYPE_CHECKING, Any + +from . import BaseDataModel +if TYPE_CHECKING: + from src.llm_models.payload_content.tool_option import ToolCall + +@dataclass +class LLMGenerationDataModel(BaseDataModel): + content: Optional[str] = None + reasoning: Optional[str] = None + model: Optional[str] = None + tool_calls: Optional[List["ToolCall"]] = None + prompt: Optional[str] = None + selected_expressions: Optional[List[int]] = None + reply_set: Optional[List[Tuple[str, Any]]] = None \ No newline at end of file diff --git a/src/common/data_models/message_data_model.py b/src/common/data_models/message_data_model.py new file mode 100644 index 000000000..8e0b77862 --- /dev/null +++ b/src/common/data_models/message_data_model.py @@ -0,0 +1,36 @@ +from typing import Optional, TYPE_CHECKING +from dataclasses import dataclass, field + +from . import BaseDataModel + +if TYPE_CHECKING: + from .database_data_model import DatabaseMessages + + +@dataclass +class MessageAndActionModel(BaseDataModel): + chat_id: str = field(default_factory=str) + time: float = field(default_factory=float) + user_id: str = field(default_factory=str) + user_platform: str = field(default_factory=str) + user_nickname: str = field(default_factory=str) + user_cardname: Optional[str] = None + processed_plain_text: Optional[str] = None + display_message: Optional[str] = None + chat_info_platform: str = field(default_factory=str) + is_action_record: bool = field(default=False) + action_name: Optional[str] = None + + @classmethod + def from_DatabaseMessages(cls, message: "DatabaseMessages"): + return cls( + chat_id=message.chat_id, + time=message.time, + user_id=message.user_info.user_id, + user_platform=message.user_info.platform, + user_nickname=message.user_info.user_nickname, + user_cardname=message.user_info.user_cardname, + processed_plain_text=message.processed_plain_text, + display_message=message.display_message, + chat_info_platform=message.chat_info.platform, + ) From 675bd4b75e7f69e89e9570642618e43db01cef45 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 12 Sep 2025 21:39:04 +0800 Subject: [PATCH 15/40] =?UTF-8?q?=E4=BC=98=E5=8C=96s4u=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/replyer/default_generator.py | 11 +++-------- src/chat/utils/prompt.py | 6 +++--- src/plugin_system/apis/cross_context_api.py | 2 +- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index b449641f4..8f8323d6f 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -86,7 +86,6 @@ def init_prompt(): ### 当前群聊中的所有人的聊天记录: {background_dialogue_prompt} -### 其他群聊中的聊天记录 {cross_context_block} ### 当前群聊中正在与你对话的聊天记录 @@ -97,14 +96,10 @@ def init_prompt(): {reply_style} {keywords_reaction_prompt} -- (如果有)你可以参考以下你在聊天中学到的表达方式: {expression_habits_block} -## 工具信息 -(如果有)你可以参考以下可能有帮助的工具返回的信息: + {tool_info_block} -## 知识库信息 -(如果有)你可以参考以下可能有帮助的知识库中的信息: {knowledge_prompt} ## 其他信息 @@ -114,8 +109,8 @@ def init_prompt(): {action_descriptions} ## 任务 -### 梗概 -- 你正在一个QQ群里聊天,你需要理解整个群的聊天动态和话题走向,并做出自然的回应。 + +*你正在一个QQ群里聊天,你需要理解整个群的聊天动态和话题走向,并做出自然的回应。* ### 核心任务 - 你现在的主要任务是和 {sender_name} 聊天。同时,也有其他用户会参与聊天,你可以参考他们的回复内容,但是你现在想回复{sender_name}的发言。 diff --git a/src/chat/utils/prompt.py b/src/chat/utils/prompt.py index ae0c9c4b1..2239bf8ab 100644 --- a/src/chat/utils/prompt.py +++ b/src/chat/utils/prompt.py @@ -549,7 +549,7 @@ class Prompt: # 构建表达习惯块 if selected_expressions: style_habits_str = "\n".join([f"- {expr}" for expr in selected_expressions]) - expression_habits_block = f"你可以参考以下的语言习惯,当情景合适就使用,但不要生硬使用,以合理的方式结合到你的回复中:\n{style_habits_str}" + expression_habits_block = f"- 你可以参考以下的语言习惯,当情景合适就使用,但不要生硬使用,以合理的方式结合到你的回复中:\n{style_habits_str}" else: expression_habits_block = "" @@ -653,7 +653,7 @@ class Prompt: # 构建工具信息块 if tool_results: - tool_info_parts = ["以下是你通过工具获取到的实时信息:"] + tool_info_parts = ["## 工具信息","以下是你通过工具获取到的实时信息:"] for tool_result in tool_results: tool_name = tool_result.get("tool_name", "unknown") content = tool_result.get("content", "") @@ -698,7 +698,7 @@ class Prompt: # 构建知识块 if knowledge_results and knowledge_results.get("knowledge_items"): - knowledge_parts = ["以下是与你当前对话相关的知识信息:"] + knowledge_parts = ["## 知识库信息","以下是与你当前对话相关的知识信息:"] for item in knowledge_results["knowledge_items"]: content = item.get("content", "") diff --git a/src/plugin_system/apis/cross_context_api.py b/src/plugin_system/apis/cross_context_api.py index 8dd4aaf97..fcc93d485 100644 --- a/src/plugin_system/apis/cross_context_api.py +++ b/src/plugin_system/apis/cross_context_api.py @@ -117,7 +117,7 @@ async def build_cross_context_s4u( if not cross_context_messages: return "" - return "# 跨上下文参考\n" + "\n\n".join(cross_context_messages) + "\n" + return "### 其他群聊中的聊天记录\n" + "\n\n".join(cross_context_messages) + "\n" async def get_chat_history_by_group_name(group_name: str) -> str: From 46b3e795bcfe5d45358de5cbb1e01086b7f2d096 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 12 Sep 2025 21:41:38 +0800 Subject: [PATCH 16/40] =?UTF-8?q?feat(emoji):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=A1=A8=E6=83=85=E9=80=89=E6=8B=A9=E6=A8=A1=E5=BC=8F=E5=B9=B6?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E9=80=89=E6=8B=A9=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引入了两种表情选择模式,允许用户通过配置决定表情选择的行为: - **emotion**: 基于情感标签进行选择,利用LLM根据对话上下文挑选最合适的情感。 - **description**: 基于表情的详细描述进行选择,让LLM从描述列表中挑选最匹配的表情。 此项重构将原有的单一选择逻辑拆分为两种独立的模式,并为每种模式优化了相应的提示词(Prompt)和处理流程,提高了表情选择的灵活性和准确性。同时,在配置文件中添加了`emoji_selection_mode`选项以支持此新功能。 --- src/config/official_configs.py | 1 + src/plugins/built_in/core_actions/emoji.py | 118 ++++++++++++++++----- template/bot_config_template.toml | 4 + 3 files changed, 96 insertions(+), 27 deletions(-) diff --git a/src/config/official_configs.py b/src/config/official_configs.py index f6fc4b3c6..4db7a512b 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -387,6 +387,7 @@ class EmojiConfig(ValidatedConfigBase): content_filtration: bool = Field(default=False, description="内容过滤") filtration_prompt: str = Field(default="符合公序良俗", description="过滤提示") enable_emotion_analysis: bool = Field(default=True, description="启用情感分析") + emoji_selection_mode: Literal["emotion", "description"] = Field(default="emotion", description="表情选择模式") max_context_emojis: int = Field(default=30, description="每次随机传递给LLM的表情包最大数量,0为全部") diff --git a/src/plugins/built_in/core_actions/emoji.py b/src/plugins/built_in/core_actions/emoji.py index 0e3305e6e..c00e9ba66 100644 --- a/src/plugins/built_in/core_actions/emoji.py +++ b/src/plugins/built_in/core_actions/emoji.py @@ -99,10 +99,72 @@ class EmojiAction(BaseAction): available_emotions = list(emotion_map.keys()) emoji_base64, emoji_description = "", "" - if not available_emotions: - logger.warning(f"{self.log_prefix} 获取到的表情包均无情感标签, 将随机发送") - emoji_base64, emoji_description = random.choice(all_emojis_data) - else: + # 4. 根据配置选择不同的表情选择模式 + if global_config.emoji.emoji_selection_mode == "emotion": + # --- 情感标签选择模式 --- + if not available_emotions: + logger.warning(f"{self.log_prefix} 获取到的表情包均无情感标签, 将随机发送") + emoji_base64, emoji_description = random.choice(all_emojis_data) + else: + # 获取最近的5条消息内容用于判断 + recent_messages = message_api.get_recent_messages(chat_id=self.chat_id, limit=5) + messages_text = "" + if recent_messages: + messages_text = message_api.build_readable_messages( + messages=recent_messages, + timestamp_mode="normal_no_YMD", + truncate=False, + show_actions=False, + ) + + # 构建prompt让LLM选择情感 + prompt = f""" + 你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个情感标签列表中选择最匹配的一个。 + 这是最近的聊天记录: + {messages_text} + + 这是理由:“{reason}” + 这里是可用的情感标签:{available_emotions} + 请直接返回最匹配的那个情感标签,不要进行任何解释或添加其他多余的文字。 + """ + + if global_config.debug.show_prompt: + logger.info(f"{self.log_prefix} 生成的LLM Prompt: {prompt}") + else: + logger.debug(f"{self.log_prefix} 生成的LLM Prompt: {prompt}") + + # 调用LLM + models = llm_api.get_available_models() + chat_model_config = models.get("planner") + if not chat_model_config: + logger.error(f"{self.log_prefix} 未找到'planner'模型配置,无法调用LLM") + return False, "未找到'planner'模型配置" + + success, chosen_emotion, _, _ = await llm_api.generate_with_model( + prompt, model_config=chat_model_config, request_type="emoji" + ) + + if not success: + logger.warning(f"{self.log_prefix} LLM调用失败: {chosen_emotion}, 将随机选择一个表情包") + emoji_base64, emoji_description = random.choice(all_emojis_data) + else: + chosen_emotion = chosen_emotion.strip().replace('"', "").replace("'", "") + logger.info(f"{self.log_prefix} LLM选择的情感: {chosen_emotion}") + + # 使用模糊匹配来查找最相关的情感标签 + matched_key = next((key for key in emotion_map if chosen_emotion in key), None) + + if matched_key: + emoji_base64, emoji_description = random.choice(emotion_map[matched_key]) + logger.info(f"{self.log_prefix} 找到匹配情感 '{chosen_emotion}' (匹配到: '{matched_key}') 的表情包: {emoji_description}") + else: + logger.warning( + f"{self.log_prefix} LLM选择的情感 '{chosen_emotion}' 不在可用列表中, 将随机选择一个表情包" + ) + emoji_base64, emoji_description = random.choice(all_emojis_data) + + elif global_config.emoji.emoji_selection_mode == "description": + # --- 详细描述选择模式 --- # 获取最近的5条消息内容用于判断 recent_messages = message_api.get_recent_messages(chat_id=self.chat_id, limit=5) messages_text = "" @@ -114,51 +176,53 @@ class EmojiAction(BaseAction): show_actions=False, ) - # 4. 构建prompt让LLM选择情感 + # 准备表情描述列表 + emoji_descriptions = [desc for _, desc in all_emojis_data] + + # 构建prompt让LLM选择描述 prompt = f""" - 你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个情感标签列表中选择最匹配的一个。 + 你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个表情包描述列表中选择最匹配的一个。 这是最近的聊天记录: {messages_text} 这是理由:“{reason}” - 这里是可用的情感标签:{available_emotions} - 请直接返回最匹配的那个情感标签,不要进行任何解释或添加其他多余的文字。 + 这里是可用的表情包描述:{emoji_descriptions} + 请直接返回最匹配的那个表情包描述,不要进行任何解释或添加其他多余的文字。 """ + logger.debug(f"{self.log_prefix} 生成的LLM Prompt: {prompt}") - if global_config.debug.show_prompt: - logger.info(f"{self.log_prefix} 生成的LLM Prompt: {prompt}") - else: - logger.debug(f"{self.log_prefix} 生成的LLM Prompt: {prompt}") - - # 5. 调用LLM + # 调用LLM models = llm_api.get_available_models() chat_model_config = models.get("planner") if not chat_model_config: - logger.error(f"{self.log_prefix} 未找到'utils_small'模型配置,无法调用LLM") - return False, "未找到'utils_small'模型配置" + logger.error(f"{self.log_prefix} 未找到'planner'模型配置,无法调用LLM") + return False, "未找到'planner'模型配置" - success, chosen_emotion, _, _ = await llm_api.generate_with_model( + success, chosen_description, _, _ = await llm_api.generate_with_model( prompt, model_config=chat_model_config, request_type="emoji" ) if not success: - logger.warning(f"{self.log_prefix} LLM调用失败: {chosen_emotion}, 将随机选择一个表情包") + logger.warning(f"{self.log_prefix} LLM调用失败: {chosen_description}, 将随机选择一个表情包") emoji_base64, emoji_description = random.choice(all_emojis_data) else: - chosen_emotion = chosen_emotion.strip().replace('"', "").replace("'", "") - logger.info(f"{self.log_prefix} LLM选择的情感: {chosen_emotion}") + chosen_description = chosen_description.strip().replace('"', "").replace("'", "") + logger.info(f"{self.log_prefix} LLM选择的描述: {chosen_description}") - # 使用模糊匹配来查找最相关的情感标签 - matched_key = next((key for key in emotion_map if chosen_emotion in key), None) - - if matched_key: - emoji_base64, emoji_description = random.choice(emotion_map[matched_key]) - logger.info(f"{self.log_prefix} 找到匹配情感 '{chosen_emotion}' (匹配到: '{matched_key}') 的表情包: {emoji_description}") + # 查找与选择的描述匹配的表情包 + matched_emoji = next((item for item in all_emojis_data if item == chosen_description), None) + + if matched_emoji: + emoji_base64, emoji_description = matched_emoji + logger.info(f"{self.log_prefix} 找到匹配描述 '{chosen_description}' 的表情包") else: logger.warning( - f"{self.log_prefix} LLM选择的情感 '{chosen_emotion}' 不在可用列表中, 将随机选择一个表情包" + f"{self.log_prefix} LLM选择的描述 '{chosen_description}' 不在可用列表中, 将随机选择一个表情包" ) emoji_base64, emoji_description = random.choice(all_emojis_data) + else: + logger.error(f"{self.log_prefix} 无效的表情选择模式: {global_config.emoji.emoji_selection_mode}") + return False, "无效的表情选择模式" # 7. 发送表情包 success = await self.send_emoji(emoji_base64) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 5472dfd73..bda368465 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -258,6 +258,10 @@ steal_emoji = true # 是否偷取表情包,让MoFox-Bot可以将一些表情 content_filtration = false # 是否启用表情包过滤,只有符合该要求的表情包才会被保存 filtration_prompt = "符合公序良俗" # 表情包过滤要求,只有符合该要求的表情包才会被保存 enable_emotion_analysis = false # 是否启用表情包感情关键词二次识别,启用后表情包在第一次识别完毕后将送入第二次大模型识别来总结感情关键词,并构建进回复和决策器的上下文消息中 +# 表情选择模式, 可选值为 "emotion" 或 "description" +# emotion: 让大模型从情感标签中选择 +# description: 让大模型从详细描述中选择 +emoji_selection_mode = "emotion" max_context_emojis = 30 # 每次随机传递给LLM的表情包详细描述的最大数量,0为全部 [memory] From 9b5eab9024986d5ed71b156db2cb8c9f5c9174db Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 12 Sep 2025 21:42:39 +0800 Subject: [PATCH 17/40] =?UTF-8?q?=E5=A6=82=E6=9E=9C=E6=88=91=E4=B8=8D?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=85=8D=E7=BD=AE=E7=89=88=E6=9C=AC=E5=8F=B7?= =?UTF-8?q?,=E9=82=A3=E6=88=91=E5=B0=B1=E4=BC=9A=E8=A2=AB=E5=85=B6?= =?UTF-8?q?=E4=BB=96=E5=BC=80=E5=8F=91=E8=8D=89=E9=A3=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/bot_config_template.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index bda368465..d5a0e6f71 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.8.5" +version = "6.8.6" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 From 5e3efd31489c0eea5cccfab7a7995529a7ca2131 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:50:37 +0800 Subject: [PATCH 18/40] =?UTF-8?q?=E6=A0=B9=E6=8D=AE=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E7=9A=84=20git=20diff=20=E5=88=86=E6=9E=90=EF=BC=8C=E8=BF=99?= =?UTF-8?q?=E6=98=AF=E4=B8=80=E4=B8=AA=E9=87=8D=E6=9E=84=E5=8F=98=E6=9B=B4?= =?UTF-8?q?=EF=BC=8C=E4=B8=BB=E8=A6=81=E6=98=AF=E4=BF=AE=E6=94=B9=E4=BA=86?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=E5=86=85=E5=AD=98=E5=8C=85=E8=A3=85=E5=99=A8?= =?UTF-8?q?=E7=9A=84=E5=AF=BC=E5=85=A5=E5=92=8C=E4=BD=BF=E7=94=A8=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=E3=80=82=E4=BB=A5=E4=B8=8B=E6=98=AF=E7=94=9F=E6=88=90?= =?UTF-8?q?=E7=9A=84=E5=B8=B8=E8=A7=84=E6=8F=90=E4=BA=A4=E6=B6=88=E6=81=AF?= =?UTF-8?q?=EF=BC=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(prompt): 重构异步即时内存包装器的导入和使用方式 - 将 async_memory 导入更改为 get_async_instant_memory 函数 - 简化内存获取逻辑,通过工厂函数创建包装器实例 - 更新参数传递方式,使用 target 替代 sender 参数 - 减少代码行数,提高可读性 --- src/chat/utils/prompt.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/chat/utils/prompt.py b/src/chat/utils/prompt.py index 88584f51d..5fc9f7842 100644 --- a/src/chat/utils/prompt.py +++ b/src/chat/utils/prompt.py @@ -567,7 +567,7 @@ class Prompt: try: from src.chat.memory_system.memory_activator import MemoryActivator - from src.chat.memory_system.async_instant_memory_wrapper import async_memory + from src.chat.memory_system.async_instant_memory_wrapper import get_async_instant_memory # 获取聊天历史 chat_history = "" @@ -589,10 +589,8 @@ class Prompt: ) # 获取即时记忆 - instant_memory = await async_memory.get_memory_with_fallback( - chat_id=self.parameters.chat_id, - target_user=self.parameters.sender - ) + async_memory_wrapper = get_async_instant_memory(self.parameters.chat_id) + instant_memory = await async_memory_wrapper.get_memory_with_fallback(self.parameters.target) # 构建记忆块 memory_parts = [] From 70511d3c264e08165c961dcb8f07a592de114253 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Sat, 13 Sep 2025 00:48:54 +0800 Subject: [PATCH 19/40] =?UTF-8?q?fix(emoji):=20=E4=BC=98=E5=8C=96=E8=A1=A8?= =?UTF-8?q?=E6=83=85=E5=8C=85=E6=8F=8F=E8=BF=B0=E5=8C=B9=E9=85=8D=E5=B9=B6?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=BD=9C=E5=9C=A8=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 改进在描述模式下选择表情包的匹配算法,从精确匹配改为更灵活的包含匹配和关键词匹配,显著提高匹配成功率。 - 初始化 `chosen_emotion` 变量,以避免在某些逻辑分支下因变量未定义而引发错误。 - 统一并简化了发送表情包后记录到prompt中的提示语,使其更加通用和简洁。 --- src/plugins/built_in/core_actions/emoji.py | 28 +++++++++++++++------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/plugins/built_in/core_actions/emoji.py b/src/plugins/built_in/core_actions/emoji.py index c00e9ba66..c29a9394b 100644 --- a/src/plugins/built_in/core_actions/emoji.py +++ b/src/plugins/built_in/core_actions/emoji.py @@ -98,6 +98,7 @@ class EmojiAction(BaseAction): available_emotions = list(emotion_map.keys()) emoji_base64, emoji_description = "", "" + chosen_emotion = "表情包" # 默认描述,避免变量未定义错误 # 4. 根据配置选择不同的表情选择模式 if global_config.emoji.emoji_selection_mode == "emotion": @@ -207,18 +208,29 @@ class EmojiAction(BaseAction): emoji_base64, emoji_description = random.choice(all_emojis_data) else: chosen_description = chosen_description.strip().replace('"', "").replace("'", "") + chosen_emotion = chosen_description # 在描述模式下,用描述作为情感标签 logger.info(f"{self.log_prefix} LLM选择的描述: {chosen_description}") - # 查找与选择的描述匹配的表情包 - matched_emoji = next((item for item in all_emojis_data if item == chosen_description), None) + # 简单关键词匹配 + matched_emoji = next((item for item in all_emojis_data if chosen_description.lower() in item[1].lower() or item[1].lower() in chosen_description.lower()), None) + + # 如果包含匹配失败,尝试关键词匹配 + if not matched_emoji: + keywords = ['惊讶', '困惑', '呆滞', '震惊', '懵', '无语', '萌', '可爱'] + for keyword in keywords: + if keyword in chosen_description: + for item in all_emojis_data: + if any(k in item[1] for k in ['呆', '萌', '惊', '困惑', '无语']): + matched_emoji = item + break + if matched_emoji: + break if matched_emoji: emoji_base64, emoji_description = matched_emoji - logger.info(f"{self.log_prefix} 找到匹配描述 '{chosen_description}' 的表情包") + logger.info(f"{self.log_prefix} 找到匹配描述的表情包: {emoji_description}") else: - logger.warning( - f"{self.log_prefix} LLM选择的描述 '{chosen_description}' 不在可用列表中, 将随机选择一个表情包" - ) + logger.warning(f"{self.log_prefix} LLM选择的描述无法匹配任何表情包, 将随机选择") emoji_base64, emoji_description = random.choice(all_emojis_data) else: logger.error(f"{self.log_prefix} 无效的表情选择模式: {global_config.emoji.emoji_selection_mode}") @@ -229,9 +241,9 @@ class EmojiAction(BaseAction): if not success: logger.error(f"{self.log_prefix} 表情包发送失败") - await self.store_action_info(action_build_into_prompt = True,action_prompt_display =f"发送了一个{chosen_emotion}的表情包,但失败了",action_done= False) + await self.store_action_info(action_build_into_prompt = True,action_prompt_display =f"发送了一个表情包,但失败了",action_done= False) return False, "表情包发送失败" - await self.store_action_info(action_build_into_prompt = True,action_prompt_display =f"发送了一个{chosen_emotion}的表情包",action_done= True) + await self.store_action_info(action_build_into_prompt = True,action_prompt_display =f"发送了一个表情包",action_done= True) return True, f"发送表情包: {emoji_description}" From bcd99553744d4013aa9d514f28a4c55737b6de54 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 13 Sep 2025 09:03:00 +0800 Subject: [PATCH 20/40] =?UTF-8?q?refactor(config):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 工具历史记录功能已被移除,因此相关的配置类 `ToolHistoryConfig` 及其在 `ToolConfig` 中的引用也一并删除,以简化代码库。 --- src/config/official_configs.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 4db7a512b..94102dce4 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -343,31 +343,11 @@ class ExpressionConfig(ValidatedConfigBase): # 如果都没有匹配,返回默认值 return True, True, 1.0 - -class ToolHistoryConfig(ValidatedConfigBase): - """工具历史记录配置类""" - - enable_history: bool = True - """是否启用工具历史记录""" - - enable_prompt_history: bool = True - """是否在提示词中加入工具历史记录""" - - max_history: int = 5 - """注入到提示词中的最大工具历史记录数量""" - - data_dir: str = "data/tool_history" - """历史记录保存目录""" - - class ToolConfig(ValidatedConfigBase): """工具配置类""" enable_tool: bool = Field(default=False, description="启用工具") - history: ToolHistoryConfig = Field(default_factory=ToolHistoryConfig) - """工具历史记录配置""" - class VoiceConfig(ValidatedConfigBase): """语音识别配置类""" From e37ee46d59caeb918f0affc4540532cbfc6e6b10 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 13 Sep 2025 11:48:04 +0800 Subject: [PATCH 21/40] =?UTF-8?q?refactor(planner):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=8A=A8=E4=BD=9C=E8=A7=84=E5=88=92=E5=99=A8=E4=B8=BA=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=8C=96=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将原有的 `ActionPlanner` 类拆分为三个独立的模块:`PlanGenerator`、`PlanFilter` 和 `PlanExecutor`。`ActionPlanner` 现在作为协调器,按顺序调用这三个模块,使规划流程更加清晰和模块化。 - **PlanGenerator**: 负责根据聊天模式和上下文生成初始规划。 - **PlanFilter**: 负责审查和筛选由生成器产生的动作。 - **PlanExecutor**: 负责执行最终确定的动作。 此重构简化了 `cycle_processor` 中的调用逻辑,并为未来的功能扩展(如更复杂的过滤规则)提供了更好的基础。同时,引入了新的 `Plan` 数据模型来统一规划过程中的数据传递。 --- src/chat/chat_loop/cycle_processor.py | 39 +- .../chat_loop/proactive/proactive_thinker.py | 219 ------ src/chat/planner_actions/plan_executor.py | 40 + src/chat/planner_actions/plan_filter.py | 327 ++++++++ src/chat/planner_actions/plan_generator.py | 82 ++ src/chat/planner_actions/planner.py | 711 +----------------- src/chat/planner_actions/planner_prompts.py | 158 ++++ src/common/data_models/info_data_model.py | 22 +- 8 files changed, 677 insertions(+), 921 deletions(-) create mode 100644 src/chat/planner_actions/plan_executor.py create mode 100644 src/chat/planner_actions/plan_filter.py create mode 100644 src/chat/planner_actions/plan_generator.py create mode 100644 src/chat/planner_actions/planner_prompts.py diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index f7bcd20fe..4bb9dddcd 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -180,7 +180,7 @@ class CycleProcessor: cycle_timers, thinking_id = self.cycle_tracker.start_cycle() logger.info(f"{self.log_prefix} 开始第{self.context.cycle_counter}次思考") - if ENABLE_S4U: + if ENABLE_S4U and self.context.chat_stream and self.context.chat_stream.user_info: await send_typing(self.context.chat_stream.user_info.user_id) loop_start_time = time.time() @@ -194,30 +194,17 @@ class CycleProcessor: logger.error(f"{self.context.log_prefix} 动作修改失败: {e}") available_actions = {} - # 执行planner - planner_info = self.action_planner.get_necessary_info() - prompt_info = await self.action_planner.build_planner_prompt( - is_group_chat=planner_info[0], - chat_target_info=planner_info[1], - current_available_actions=planner_info[2], - ) - from src.plugin_system.core.event_manager import event_manager - from src.plugin_system import EventType - - # 触发规划前事件 - result = await event_manager.trigger_event( - EventType.ON_PLAN, permission_group="SYSTEM", stream_id=self.context.chat_stream - ) - if not result.all_continue_process(): - raise UserWarning(f"插件{result.get_summary().get('stopped_handlers', '')}于规划前中断了内容生成") - # 规划动作 - with Timer("规划器", cycle_timers): - actions, _ = await self.action_planner.plan( - mode=mode, - loop_start_time=loop_start_time, - available_actions=available_actions, - ) + from src.plugin_system.core.event_manager import event_manager + from src.plugin_system import EventType + + result = await event_manager.trigger_event( + EventType.ON_PLAN, permission_group="SYSTEM", stream_id=self.context.chat_stream + ) + if not result.all_continue_process(): + raise UserWarning(f"插件{result.get_summary().get('stopped_handlers', '')}于规划前中断了内容生成") + with Timer("规划器", cycle_timers): + actions, _ = await self.action_planner.plan(mode=mode) async def execute_action(action_info): """执行单个动作的通用函数""" @@ -373,7 +360,7 @@ class CycleProcessor: self.context.chat_instance.cycle_tracker.end_cycle(loop_info, cycle_timers) self.context.chat_instance.cycle_tracker.print_cycle_info(cycle_timers) - action_type = actions[0]["action_type"] if actions else "no_action" + action_type = actions["action_type"] if actions else "no_action" return action_type async def _handle_action( @@ -424,7 +411,7 @@ class CycleProcessor: if "reply" in available_actions: fallback_action = "reply" elif available_actions: - fallback_action = list(available_actions.keys())[0] + fallback_action = list(available_actions.keys()) if fallback_action and fallback_action != action: logger.info(f"{self.context.log_prefix} 使用回退动作: {fallback_action}") diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py index 3a6af40fe..8f304b406 100644 --- a/src/chat/chat_loop/proactive/proactive_thinker.py +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -117,64 +117,6 @@ class ProactiveThinker: trigger_event (ProactiveTriggerEvent): 触发事件。 """ try: - # 如果是提醒事件,直接使用当前上下文执行at_user动作 - if trigger_event.source == "reminder_system": - # 1. 获取上下文信息 - metadata = trigger_event.metadata or {} - reminder_content = trigger_event.reason.replace("定时提醒:", "").strip() - - # 2. 使用LLM智能解析目标用户名 - target_user_name = None - - # 首先尝试从完整的原始信息中解析(如果有的话) - full_content = trigger_event.reason - logger.info(f"{self.context.log_prefix} 解析提醒内容: '{full_content}'") - - sender_name = metadata.get("sender_name") - target_user_name = await self._extract_target_user_with_llm(full_content, sender_name) - - if not target_user_name: - logger.warning(f"无法从提醒 '{reminder_content}' 中确定目标用户,回退") - # 回退到生成普通提醒消息 - fallback_action = { - "action_type": "proactive_reply", - "action_data": {"topic": f"定时提醒:{reminder_content}"}, - "action_message": metadata - } - await self._generate_reminder_proactive_reply(fallback_action, trigger_event, reminder_content) - return - - # 3. 直接使用当前上下文的cycle_processor执行at_user动作 - try: - success, _, _ = await self.cycle_processor._handle_action( - action="at_user", - reasoning="执行定时提醒", - action_data={ - "user_name": target_user_name, - "at_message": reminder_content - }, - cycle_timers={}, - thinking_id="", - action_message=metadata, - ) - if success: - logger.info(f"{self.context.log_prefix} 成功执行定时提醒艾特用户 {target_user_name}") - return - else: - raise Exception("at_user action failed") - except Exception as e: - logger.warning(f"{self.context.log_prefix} at_user动作执行失败: {e},回退到专用提醒回复") - # 回退到专用的定时提醒回复 - fallback_action = { - "action_type": "proactive_reply", - "action_data": {"topic": f"定时提醒:{reminder_content}"}, - "action_message": metadata - } - await self._generate_reminder_proactive_reply(fallback_action, trigger_event, reminder_content) - return - - else: - # 对于其他来源的主动思考,正常调用规划器 actions, _ = await self.cycle_processor.action_planner.plan(mode=ChatMode.PROACTIVE) action_result = actions[0] if actions else {} action_type = action_result.get("action_type") @@ -196,164 +138,7 @@ class ProactiveThinker: except Exception as e: logger.error(f"{self.context.log_prefix} 主动思考执行异常: {e}") logger.error(traceback.format_exc()) - async def _extract_target_user_with_llm(self, reminder_content: str, sender_name: str) -> str: - """ - 使用LLM从提醒内容中提取目标用户名 - Args: - reminder_content: 完整的提醒内容 - sender_name: 消息发送者的昵称 - - Returns: - 提取出的用户名,如果找不到则返回None - """ - try: - from src.llm_models.utils_model import LLMRequest - from src.config.config import model_config - - bot_name = global_config.bot.nickname - user_extraction_prompt = f''' -从以下提醒消息中提取需要被提醒的目标用户名。 - -**重要认知**: -- 你的名字是"{bot_name}"。当消息中提到"{bot_name}"时,通常是在称呼你。 -- 消息的发送者是"{sender_name}"。当消息中出现"我"、"咱"等第一人称代词时,指代的就是"{sender_name}"。 - -提醒消息: "{reminder_content}" - -规则: -1. 分析消息,找出真正需要被提醒的人。 -2. 如果提醒目标是第一人称(如"我"),那么目标就是发送者"{sender_name}"。 -3. **绝对不能**提取你自己的名字("{bot_name}")作为目标。 -4. 只提取最关键的人名,不要包含多余的词语。 -5. 如果没有明确的提醒目标(既不是其他人,也不是发送者自己),请回答"无"。 - -示例: -- 消息: "定时提醒:{bot_name},10分钟后提醒我去打深渊" -> "{sender_name}" -- 消息: "定时提醒:{bot_name},提醒阿范一分钟后去写模组" -> "阿范" -- 消息: "定时提醒:一分钟后提醒一闪喝水" -> "一闪" -- 消息: "定时提醒:喝水" -> "无" -- 消息: "定时提醒:{bot_name},记得休息" -> "无" - -请直接输出提取到的用户名,如果不存在则输出"无"。 -''' - - llm_request = LLMRequest( - model_set=model_config.model_task_config.utils_small, - request_type="reminder_user_extraction" - ) - - response, _ = await llm_request.generate_response_async(prompt=user_extraction_prompt) - - if response and response.strip() != "无": - logger.info(f"LLM成功提取目标用户: '{response.strip()}'") - return response.strip() - else: - logger.warning(f"LLM未能从 '{reminder_content}' 中提取目标用户") - return None - - except Exception as e: - logger.error(f"使用LLM提取用户名时出错: {e}") - return None - - async def _generate_reminder_proactive_reply(self, action_result: Dict[str, Any], trigger_event: ProactiveTriggerEvent, reminder_content: str): - """ - 为定时提醒事件生成专用的主动回复 - - Args: - action_result: 动作结果 - trigger_event: 触发事件 - reminder_content: 提醒内容 - """ - try: - logger.info(f"{self.context.log_prefix} 生成定时提醒专用回复: '{reminder_content}'") - - # 获取基本信息 - bot_name = global_config.bot.nickname - personality = global_config.personality - identity_block = ( - f"你的名字是{bot_name}。\n" - f"关于你:{personality.personality_core},并且{personality.personality_side}。\n" - f"你的身份是{personality.identity},平时说话风格是{personality.reply_style}。" - ) - mood_block = f"你现在的心情是:{mood_manager.get_mood_by_chat_id(self.context.stream_id).mood_state}" - - # 获取日程信息 - schedule_block = "你今天没有日程安排。" - if global_config.planning_system.schedule_enable: - if current_activity := schedule_manager.get_current_activity(): - schedule_block = f"你当前正在:{current_activity}。" - - # 为定时提醒定制的专用提示词 - reminder_prompt = f""" -## 你的角色 -{identity_block} - -## 你的心情 -{mood_block} - -## 你今天的日程安排 -{schedule_block} - -## 定时提醒任务 -你收到了一个定时提醒:"{reminder_content}" -这是一个自动触发的提醒事件,你需要根据提醒内容发送一条友好的提醒消息。 - -## 任务要求 -- 这是一个定时提醒,要体现出你的贴心和关怀 -- 根据提醒内容的具体情况(如"喝水"、"休息"等)给出相应的提醒 -- 保持你一贯的温暖、俏皮风格 -- 可以加上一些鼓励或关心的话语 -- 直接输出提醒消息,不要解释为什么要提醒 - -请生成一条温暖贴心的提醒消息。 -""" - - response_text = await generator_api.generate_response_custom( - chat_stream=self.context.chat_stream, - prompt=reminder_prompt, - request_type="chat.replyer.reminder", - ) - - if response_text: - response_set = process_human_text( - content=response_text, - enable_splitter=global_config.response_splitter.enable, - enable_chinese_typo=global_config.chinese_typo.enable, - ) - await self.cycle_processor.response_handler.send_response( - response_set, time.time(), action_result.get("action_message") - ) - await store_action_info( - chat_stream=self.context.chat_stream, - action_name="reminder_reply", - action_data={"reminder_content": reminder_content, "response": response_text}, - action_prompt_display=f"定时提醒回复: {reminder_content}", - action_done=True, - ) - logger.info(f"{self.context.log_prefix} 成功发送定时提醒回复: {response_text}") - else: - logger.error(f"{self.context.log_prefix} 定时提醒回复生成失败。") - - except Exception as e: - logger.error(f"{self.context.log_prefix} 生成定时提醒回复时异常: {e}") - logger.error(traceback.format_exc()) - - - async def _get_reminder_context(self, message_id: str) -> str: - """获取提醒消息的上下文""" - try: - # 只获取那一条消息 - message_record = await db_get(Messages, {"message_id": message_id}, single_result=True) - if message_record: - # 使用 build_readable_messages_with_id 来格式化单条消息 - chat_context_block, _ = build_readable_messages_with_id(messages=[message_record]) - return chat_context_block - return "无法加载相关的聊天记录。" - except Exception as e: - logger.error(f"{self.context.log_prefix} 获取提醒上下文失败: {e}") - return "无法加载相关的聊天记录。" - async def _generate_proactive_content_and_send(self, action_result: Dict[str, Any], trigger_event: ProactiveTriggerEvent): """ 获取实时信息,构建最终的生成提示词,并生成和发送主动回复。 @@ -394,10 +179,6 @@ class ProactiveThinker: logger.warning(f"{self.context.log_prefix} 未找到 web_search 工具实例。") except Exception as e: logger.error(f"{self.context.log_prefix} 主动思考时网络搜索失败: {e}") - - if trigger_event.source == "reminder_system" and trigger_event.related_message_id: - chat_context_block = await self._get_reminder_context(trigger_event.related_message_id) - else: message_list = get_raw_msg_before_timestamp_with_chat( chat_id=self.context.stream_id, timestamp=time.time(), diff --git a/src/chat/planner_actions/plan_executor.py b/src/chat/planner_actions/plan_executor.py new file mode 100644 index 000000000..6cc828a64 --- /dev/null +++ b/src/chat/planner_actions/plan_executor.py @@ -0,0 +1,40 @@ +""" +PlanExecutor: 接收 Plan 对象并执行其中的所有动作。 +""" +from src.chat.planner_actions.action_manager import ActionManager +from src.common.data_models.info_data_model import Plan +from src.common.logger import get_logger + +logger = get_logger("plan_executor") + + +class PlanExecutor: + """ + 执行 Plan 中最终确定的动作。 + """ + + def __init__(self, action_manager: ActionManager): + self.action_manager = action_manager + + async def execute(self, plan: Plan): + """ + 读取 Plan 对象的 decided_actions 字段并执行。 + """ + if not plan.decided_actions: + logger.info("没有需要执行的动作。") + return + + for action_info in plan.decided_actions: + if action_info.action_type == "no_action": + logger.info(f"规划器决策不执行动作,原因: {action_info.reasoning}") + continue + + # TODO: 对接 ActionManager 的执行方法 + # 这是一个示例调用,需要根据 ActionManager 的最终实现进行调整 + logger.info(f"执行动作: {action_info.action_type}, 原因: {action_info.reasoning}") + # await self.action_manager.execute_action( + # action_name=action_info.action_type, + # action_data=action_info.action_data, + # reasoning=action_info.reasoning, + # action_message=action_info.action_message, + # ) diff --git a/src/chat/planner_actions/plan_filter.py b/src/chat/planner_actions/plan_filter.py new file mode 100644 index 000000000..00097870c --- /dev/null +++ b/src/chat/planner_actions/plan_filter.py @@ -0,0 +1,327 @@ +""" +PlanFilter: 接收 Plan 对象,根据不同模式的逻辑进行筛选,决定最终要执行的动作。 +""" +import orjson +import time +import traceback +from datetime import datetime +from typing import Any, Dict, List, Optional + +from json_repair import repair_json + +from src.chat.memory_system.Hippocampus import hippocampus_manager +from src.chat.utils.chat_message_builder import ( + build_readable_actions, + build_readable_messages_with_id, + get_actions_by_timestamp_with_chat, +) +from src.chat.utils.prompt import global_prompt_manager +from src.common.data_models.info_data_model import ActionPlannerInfo, Plan +from src.common.logger import get_logger +from src.config.config import global_config, model_config +from src.llm_models.utils_model import LLMRequest +from src.mood.mood_manager import mood_manager +from src.plugin_system.base.component_types import ActionInfo, ChatMode +from src.schedule.schedule_manager import schedule_manager + +logger = get_logger("plan_filter") + + +class PlanFilter: + """ + 根据 Plan 中的模式和信息,筛选并决定最终的动作。 + """ + + def __init__(self): + self.planner_llm = LLMRequest( + model_set=model_config.model_task_config.planner, request_type="planner" + ) + self.last_obs_time_mark = 0.0 + + async def filter(self, plan: Plan) -> Plan: + """ + 执行筛选逻辑,并填充 Plan 对象的 decided_actions 字段。 + """ + try: + prompt, used_message_id_list = await self._build_prompt(plan) + plan.llm_prompt = prompt + + llm_content, _ = await self.planner_llm.generate_response_async(prompt=prompt) + + if llm_content: + parsed_json = orjson.loads(repair_json(llm_content)) + + if isinstance(parsed_json, dict): + parsed_json = [parsed_json] + + if isinstance(parsed_json, list): + final_actions = [] + for item in parsed_json: + if isinstance(item, dict): + final_actions.extend( + await self._parse_single_action( + item, used_message_id_list, plan + ) + ) + plan.decided_actions = self._filter_no_actions(final_actions) + + except Exception as e: + logger.error(f"筛选 Plan 时出错: {e}\n{traceback.format_exc()}") + plan.decided_actions = [ + ActionPlannerInfo(action_type="no_action", reasoning=f"筛选时出错: {e}") + ] + + return plan + + async def _build_prompt(self, plan: Plan) -> tuple[str, list]: + """ + 根据 Plan 对象构建提示词。 + """ + try: + time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + bot_name = global_config.bot.nickname + bot_nickname = ( + f",也有人叫你{','.join(global_config.bot.alias_names)}" if global_config.bot.alias_names else "" + ) + bot_core_personality = global_config.personality.personality_core + identity_block = f"你的名字是{bot_name}{bot_nickname},你{bot_core_personality}:" + + schedule_block = "" + if global_config.planning_system.schedule_enable: + if current_activity := schedule_manager.get_current_activity(): + schedule_block = f"你当前正在:{current_activity},但注意它与群聊的聊天无关。" + + mood_block = "" + if global_config.mood.enable_mood: + chat_mood = mood_manager.get_mood_by_chat_id(plan.chat_id) + mood_block = f"你现在的心情是:{chat_mood.mood_state}" + + if plan.mode == ChatMode.PROACTIVE: + long_term_memory_block = await self._get_long_term_memory_context() + + chat_content_block, message_id_list = build_readable_messages_with_id( + messages=[msg.flatten() for msg in plan.chat_history], + timestamp_mode="normal", + truncate=False, + show_actions=False, + ) + + prompt_template = await global_prompt_manager.get_prompt_async("proactive_planner_prompt") + actions_before_now = get_actions_by_timestamp_with_chat( + chat_id=plan.chat_id, + timestamp_start=time.time() - 3600, + timestamp_end=time.time(), + limit=5, + ) + actions_before_now_block = build_readable_actions(actions=actions_before_now) + actions_before_now_block = f"你刚刚选择并执行过的action是:\n{actions_before_now_block}" + + prompt = prompt_template.format( + time_block=time_block, + identity_block=identity_block, + schedule_block=schedule_block, + mood_block=mood_block, + long_term_memory_block=long_term_memory_block, + chat_content_block=chat_content_block or "最近没有聊天内容。", + actions_before_now_block=actions_before_now_block, + ) + return prompt, message_id_list + + chat_content_block, message_id_list = build_readable_messages_with_id( + messages=[msg.flatten() for msg in plan.chat_history], + timestamp_mode="normal", + read_mark=self.last_obs_time_mark, + truncate=True, + show_actions=True, + ) + + actions_before_now = get_actions_by_timestamp_with_chat( + chat_id=plan.chat_id, + timestamp_start=time.time() - 3600, + timestamp_end=time.time(), + limit=5, + ) + + actions_before_now_block = build_readable_actions(actions=actions_before_now) + actions_before_now_block = f"你刚刚选择并执行过的action是:\n{actions_before_now_block}" + + self.last_obs_time_mark = time.time() + + mentioned_bonus = "" + if global_config.chat.mentioned_bot_inevitable_reply: + mentioned_bonus = "\n- 有人提到你" + if global_config.chat.at_bot_inevitable_reply: + mentioned_bonus = "\n- 有人提到你,或者at你" + + if plan.mode == ChatMode.FOCUS: + no_action_block = """ +动作:no_action +动作描述:不选择任何动作 +{{ + "action": "no_action", + "reason":"不动作的原因" +}} + +动作:no_reply +动作描述:不进行回复,等待合适的回复时机 +- 当你刚刚发送了消息,没有人回复时,选择no_reply +- 当你一次发送了太多消息,为了避免打扰聊天节奏,选择no_reply +{{ + "action": "no_reply", + "reason":"不回复的原因" +}} +""" + else: # NORMAL Mode + no_action_block = """重要说明: +- 'reply' 表示只进行普通聊天回复,不执行任何额外动作 +- 其他action表示在普通回复的基础上,执行相应的额外动作 +{{ + "action": "reply", + "target_message_id":"触发action的消息id", + "reason":"回复的原因" +}}""" + + is_group_chat = plan.target_info.platform == "group" if plan.target_info else True + chat_context_description = "你现在正在一个群聊中" + if not is_group_chat and plan.target_info: + chat_target_name = plan.target_info.person_name or plan.target_info.user_nickname or "对方" + chat_context_description = f"你正在和 {chat_target_name} 私聊" + + action_options_block = await self._build_action_options(plan.available_actions) + + moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。" + + custom_prompt_block = "" + if global_config.custom_prompt.planner_custom_prompt_content: + custom_prompt_block = global_config.custom_prompt.planner_custom_prompt_content + + users_in_chat_str = "" # TODO: Re-implement user list fetching if needed + + planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt") + prompt = planner_prompt_template.format( + schedule_block=schedule_block, + mood_block=mood_block, + time_block=time_block, + chat_context_description=chat_context_description, + chat_content_block=chat_content_block, + actions_before_now_block=actions_before_now_block, + mentioned_bonus=mentioned_bonus, + no_action_block=no_action_block, + action_options_text=action_options_block, + moderation_prompt=moderation_prompt_block, + identity_block=identity_block, + custom_prompt_block=custom_prompt_block, + bot_name=bot_name, + users_in_chat=users_in_chat_str + ) + return prompt, message_id_list + except Exception as e: + logger.error(f"构建 Planner 提示词时出错: {e}") + logger.error(traceback.format_exc()) + return "构建 Planner Prompt 时出错", [] + + async def _parse_single_action( + self, action_json: dict, message_id_list: list, plan: Plan + ) -> List[ActionPlannerInfo]: + parsed_actions = [] + try: + action = action_json.get("action", "no_action") + reasoning = action_json.get("reason", "未提供原因") + action_data = {k: v for k, v in action_json.items() if k not in ["action", "reason"]} + + target_message_obj = None + if action not in ["no_action", "no_reply", "do_nothing", "proactive_reply"]: + if target_message_id := action_json.get("target_message_id"): + target_message_dict = self._find_message_by_id(target_message_id, message_id_list) + if target_message_dict is None: + target_message_dict = self._get_latest_message(message_id_list) + if target_message_dict: + from src.common.data_models.database_data_model import DatabaseMessages + target_message_obj = DatabaseMessages(**target_message_dict) + + available_action_names = list(plan.available_actions.keys()) + if action not in ["no_action", "no_reply", "reply", "do_nothing", "proactive_reply"] and action not in available_action_names: + reasoning = f"LLM 返回了当前不可用的动作 '{action}'。原始理由: {reasoning}" + action = "no_action" + + parsed_actions.append( + ActionPlannerInfo( + action_type=action, + reasoning=reasoning, + action_data=action_data, + action_message=target_message_obj, + available_actions=plan.available_actions, + ) + ) + except Exception as e: + logger.error(f"解析单个action时出错: {e}") + parsed_actions.append( + ActionPlannerInfo( + action_type="no_action", + reasoning=f"解析action时出错: {e}", + ) + ) + return parsed_actions + + def _filter_no_actions( + self, action_list: List[ActionPlannerInfo] + ) -> List[ActionPlannerInfo]: + non_no_actions = [a for a in action_list if a.action_type not in ["no_action", "no_reply"]] + if non_no_actions: + return non_no_actions + return action_list[:1] if action_list else [] + + async def _get_long_term_memory_context(self) -> str: + try: + now = datetime.now() + keywords = ["今天", "日程", "计划"] + if 5 <= now.hour < 12: + keywords.append("早上") + elif 12 <= now.hour < 18: + keywords.append("中午") + else: + keywords.append("晚上") + + retrieved_memories = await hippocampus_manager.get_memory_from_topic( + valid_keywords=keywords, max_memory_num=5, max_memory_length=1 + ) + + if not retrieved_memories: + return "最近没有什么特别的记忆。" + + memory_statements = [f"关于'{topic}', 你记得'{memory_item}'。" for topic, memory_item in retrieved_memories] + return " ".join(memory_statements) + except Exception as e: + logger.error(f"获取长期记忆时出错: {e}") + return "回忆时出现了一些问题。" + + async def _build_action_options(self, current_available_actions: Dict[str, ActionInfo]) -> str: + action_options_block = "" + for action_name, action_info in current_available_actions.items(): + param_text = "" + if action_info.action_parameters: + param_text = "\n" + "\n".join( + f' "{p_name}":"{p_desc}"' for p_name, p_desc in action_info.action_parameters.items() + ) + require_text = "\n".join(f"- {req}" for req in action_info.action_require) + using_action_prompt = await global_prompt_manager.get_prompt_async("action_prompt") + action_options_block += using_action_prompt.format( + action_name=action_name, + action_description=action_info.description, + action_parameters=param_text, + action_require=require_text, + ) + return action_options_block + + def _find_message_by_id(self, message_id: str, message_id_list: list) -> Optional[Dict[str, Any]]: + if message_id.isdigit(): + message_id = f"m{message_id}" + for item in message_id_list: + if item.get("id") == message_id: + return item.get("message") + return None + + def _get_latest_message(self, message_id_list: list) -> Optional[Dict[str, Any]]: + if not message_id_list: + return None + return message_id_list[-1].get("message") diff --git a/src/chat/planner_actions/plan_generator.py b/src/chat/planner_actions/plan_generator.py new file mode 100644 index 000000000..e3b08eff1 --- /dev/null +++ b/src/chat/planner_actions/plan_generator.py @@ -0,0 +1,82 @@ +""" +PlanGenerator: 负责搜集和汇总所有决策所需的信息,生成一个未经筛选的“原始计划” (Plan)。 +""" +import time +from typing import Dict, Optional, Tuple + +from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat +from src.chat.utils.utils import get_chat_type_and_target_info +from src.common.data_models.database_data_model import DatabaseMessages +from src.common.data_models.info_data_model import Plan, TargetPersonInfo +from src.config.config import global_config +from src.plugin_system.base.component_types import ActionInfo, ChatMode, ComponentType +from src.plugin_system.core.component_registry import component_registry + + +class PlanGenerator: + """ + 搜集信息并生成初始 Plan 对象。 + """ + + def __init__(self, chat_id: str): + from src.chat.planner_actions.action_manager import ActionManager + self.chat_id = chat_id + # 注意:ActionManager 可能需要根据实际情况初始化 + self.action_manager = ActionManager() + + async def generate(self, mode: ChatMode) -> Plan: + """ + 生成并填充初始的 Plan 对象。 + """ + _is_group_chat, chat_target_info_dict = get_chat_type_and_target_info(self.chat_id) + + target_info = None + if chat_target_info_dict: + target_info = TargetPersonInfo(**chat_target_info_dict) + + available_actions = self._get_available_actions() + + chat_history_raw = get_raw_msg_before_timestamp_with_chat( + chat_id=self.chat_id, + timestamp=time.time(), + limit=int(global_config.chat.max_context_size), + ) + chat_history = [DatabaseMessages(**msg) for msg in chat_history_raw] + + + plan = Plan( + chat_id=self.chat_id, + mode=mode, + available_actions=available_actions, + chat_history=chat_history, + target_info=target_info, + ) + return plan + + def _get_available_actions(self) -> Dict[str, "ActionInfo"]: + """ + 获取当前可用的动作。 + """ + current_available_actions_dict = self.action_manager.get_using_actions() + all_registered_actions: Dict[str, ActionInfo] = component_registry.get_components_by_type( # type: ignore + ComponentType.ACTION + ) + + current_available_actions = {} + for action_name in current_available_actions_dict: + if action_name in all_registered_actions: + current_available_actions[action_name] = all_registered_actions[action_name] + + no_reply_info = ActionInfo( + name="no_reply", + component_type=ComponentType.ACTION, + description="系统级动作:选择不回复消息的决策", + action_parameters={}, + activation_keywords=[], + plugin_name="SYSTEM", + enabled=True, + parallel_action=False, + ) + current_available_actions["no_reply"] = no_reply_info + + return current_available_actions \ No newline at end of file diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index cdab8b3d6..52b879108 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -1,694 +1,57 @@ -import orjson -import time -import traceback -import random -from typing import Dict, Any, Optional, Tuple, List, TYPE_CHECKING -from rich.traceback import install -from datetime import datetime -from json_repair import repair_json +""" +主规划器入口,负责协调 PlanGenerator, PlanFilter, 和 PlanExecutor。 +""" +from dataclasses import asdict +from typing import Dict, List, Optional, Tuple -from src.llm_models.utils_model import LLMRequest -from src.config.config import global_config, model_config -from src.common.logger import get_logger -from src.chat.utils.prompt import Prompt, global_prompt_manager -from src.chat.utils.chat_message_builder import ( - build_readable_actions, - get_actions_by_timestamp_with_chat, - build_readable_messages_with_id, - get_raw_msg_before_timestamp_with_chat, -) -from src.chat.utils.utils import get_chat_type_and_target_info from src.chat.planner_actions.action_manager import ActionManager -from src.chat.message_receive.chat_stream import get_chat_manager -from src.plugin_system.base.component_types import ( - ActionInfo, - ChatMode, - ComponentType, -) -from src.plugin_system.core.component_registry import component_registry -from src.schedule.schedule_manager import schedule_manager -from src.mood.mood_manager import mood_manager -from src.chat.memory_system.Hippocampus import hippocampus_manager +from src.chat.planner_actions.plan_executor import PlanExecutor +from src.chat.planner_actions.plan_filter import PlanFilter +from src.chat.planner_actions.plan_generator import PlanGenerator +from src.common.data_models.info_data_model import ActionPlannerInfo +from src.common.logger import get_logger +from src.plugin_system.base.component_types import ChatMode -if TYPE_CHECKING: - pass +# 导入提示词模块以确保其被初始化 +from . import planner_prompts logger = get_logger("planner") -install(extra_lines=3) - - -def init_prompt(): - Prompt( - """ -{schedule_block} -{mood_block} -{time_block} -{identity_block} - -{users_in_chat} -{custom_prompt_block} -{chat_context_description},以下是具体的聊天内容。 -{chat_content_block} - -{moderation_prompt} - -**任务: 构建一个完整的响应** -你的任务是根据当前的聊天内容,构建一个完整的、人性化的响应。一个完整的响应由两部分组成: -1. **主要动作**: 这是响应的核心,通常是 `reply`(文本回复)。 -2. **辅助动作 (可选)**: 这是为了增强表达效果的附加动作,例如 `emoji`(发送表情包)或 `poke_user`(戳一戳)。 - -**决策流程:** -1. 首先,决定是否要进行 `reply`。 -2. 然后,评估当前的对话气氛和用户情绪,判断是否需要一个**辅助动作**来让你的回应更生动、更符合你的性格。 -3. 如果需要,选择一个最合适的辅助动作与 `reply` 组合。 -4. 如果用户明确要求了某个动作,请务必优先满足。 - -**可用动作:** -{actions_before_now_block} - -{no_action_block} - -动作:reply -动作描述:参与聊天回复,发送文本进行表达 -- 你想要闲聊或者随便附和 -- {mentioned_bonus} -- 如果你刚刚进行了回复,不要对同一个话题重复回应 -- 不要回复自己发送的消息 -{{ - "action": "reply", - "target_message_id": "触发action的消息id", - "reason": "回复的原因" -}} - -{action_options_text} - - -**输出格式:** -你必须以严格的 JSON 格式输出,返回一个包含所有选定动作的JSON列表。如果没有任何合适的动作,返回一个空列表[]。 - -**单动作示例 (仅回复):** -[ - {{ - "action": "reply", - "target_message_id": "m123", - "reason": "回答用户的问题" - }} -] - -**组合动作示例 (回复 + 表情包):** -[ - {{ - "action": "reply", - "target_message_id": "m123", - "reason": "回答用户的问题" - }}, - {{ - "action": "emoji", - "target_message_id": "m123", - "reason": "用一个可爱的表情来缓和气氛" - }} -] - -不要输出markdown格式```json等内容,直接输出且仅包含 JSON 列表内容: -""", - "planner_prompt", - ) - - Prompt( - """ -# 主动思考决策 - -## 你的内部状态 -{time_block} -{identity_block} -{schedule_block} -{mood_block} - -## 长期记忆摘要 -{long_term_memory_block} - -## 最近的聊天内容 -{chat_content_block} - -## 最近的动作历史 -{actions_before_now_block} - -## 任务 -你现在要决定是否主动说些什么。就像一个真实的人一样,有时候会突然想起之前聊到的话题,或者对朋友的近况感到好奇,想主动询问或关心一下。 - -请基于聊天内容,用你的判断力来决定是否要主动发言。不要按照固定规则,而是像人类一样自然地思考: -- 是否想起了什么之前提到的事情,想问问后来怎么样了? -- 是否注意到朋友提到了什么值得关心的事情? -- 是否有什么话题突然想到,觉得现在聊聊很合适? -- 或者觉得现在保持沉默更好? - -## 可用动作 -动作:proactive_reply -动作描述:主动发起对话,可以是关心朋友、询问近况、延续之前的话题,或分享想法。 -- 当你突然想起之前的话题,想询问进展时 -- 当你想关心朋友的情况时 -- 当你有什么想法想分享时 -- 当你觉得现在是个合适的聊天时机时 -{{ - "action": "proactive_reply", - "reason": "你决定主动发言的具体原因", - "topic": "你想说的内容主题(简洁描述)" -}} - -动作:do_nothing -动作描述:保持沉默,不主动发起对话。 -- 当你觉得现在不是合适的时机时 -- 当最近已经说得够多了时 -- 当对话氛围不适合插入时 -{{ - "action": "do_nothing", - "reason": "决定保持沉默的原因" -}} - -你必须从上面列出的可用action中选择一个。要像真人一样自然地思考和决策。 -请以严格的 JSON 格式输出,且仅包含 JSON 内容: -""", - "proactive_planner_prompt", - ) - - Prompt( - """ -动作:{action_name} -动作描述:{action_description} -{action_require} -{{ - "action": "{action_name}", - "target_message_id": "触发action的消息id", - "reason": "触发action的原因"{action_parameters} -}} -""", - "action_prompt", - ) - class ActionPlanner: + """ + 协调器,按顺序调用 Generator -> Filter -> Executor。 + """ + def __init__(self, chat_id: str, action_manager: ActionManager): self.chat_id = chat_id - self.log_prefix = f"[{get_chat_manager().get_stream_name(chat_id) or chat_id}]" self.action_manager = action_manager - # LLM规划器配置 - # --- 大脑 --- - self.planner_llm = LLMRequest( - model_set=model_config.model_task_config.planner, request_type="planner" - ) - self.last_obs_time_mark = 0.0 - - async def _get_long_term_memory_context(self) -> str: - """ - 获取长期记忆上下文 - """ - try: - # 1. 生成时间相关的关键词 - now = datetime.now() - keywords = ["今天", "日程", "计划"] - if 5 <= now.hour < 12: - keywords.append("早上") - elif 12 <= now.hour < 18: - keywords.append("中午") - else: - keywords.append("晚上") - - # TODO: 添加与聊天对象相关的关键词 - - # 2. 调用 hippocampus_manager 检索记忆 - retrieved_memories = await hippocampus_manager.get_memory_from_topic( - valid_keywords=keywords, max_memory_num=5, max_memory_length=1 - ) - - if not retrieved_memories: - return "最近没有什么特别的记忆。" - - # 3. 格式化记忆 - memory_statements = [] - for topic, memory_item in retrieved_memories: - memory_statements.append(f"关于'{topic}', 你记得'{memory_item}'。") - - return " ".join(memory_statements) - except Exception as e: - logger.error(f"获取长期记忆时出错: {e}") - return "回忆时出现了一些问题。" - - async def _build_action_options( - self, - current_available_actions: Dict[str, ActionInfo], - mode: ChatMode, - target_prompt: str = "", - ) -> str: - """ - 构建动作选项 - """ - action_options_block = "" - for action_name, action_info in current_available_actions.items(): - # TODO: 增加一个字段来判断action是否支持在PROACTIVE模式下使用 - - param_text = "" - if action_info.action_parameters: - param_text = "\n" + "\n".join( - f' "{p_name}":"{p_desc}"' for p_name, p_desc in action_info.action_parameters.items() - ) - - require_text = "\n".join(f"- {req}" for req in action_info.action_require) - - using_action_prompt = await global_prompt_manager.get_prompt_async("action_prompt") - action_options_block += using_action_prompt.format( - action_name=action_name, - action_description=action_info.description, - action_parameters=param_text, - action_require=require_text, - ) - return action_options_block - - def find_message_by_id(self, message_id: str, message_id_list: list) -> Optional[Dict[str, Any]]: - # sourcery skip: use-next - """ - 根据message_id从message_id_list中查找对应的原始消息 - - Args: - message_id: 要查找的消息ID - message_id_list: 消息ID列表,格式为[{'id': str, 'message': dict}, ...] - - Returns: - 找到的原始消息字典,如果未找到则返回None - """ - # 检测message_id 是否为纯数字 - if message_id.isdigit(): - message_id = f"m{message_id}" - for item in message_id_list: - if item.get("id") == message_id: - return item.get("message") - return None - - def get_latest_message(self, message_id_list: list) -> Optional[Dict[str, Any]]: - """ - 获取消息列表中的最新消息 - - Args: - message_id_list: 消息ID列表,格式为[{'id': str, 'message': dict}, ...] - - Returns: - 最新的消息字典,如果列表为空则返回None - """ - if not message_id_list: - return None - # 假设消息列表是按时间顺序排列的,最后一个是最新的 - return message_id_list[-1].get("message") - - async def _parse_single_action( - self, - action_json: dict, - message_id_list: list, # 使用 planner.py 的 list of dict - current_available_actions: list, # 使用 planner.py 的 list of tuple - ) -> List[Dict[str, Any]]: - """ - [注释] 解析单个LLM返回的action JSON,并将其转换为标准化的字典。 - """ - parsed_actions = [] - try: - action = action_json.get("action", "no_action") - reasoning = action_json.get("reason", "未提供原因") - action_data = {k: v for k, v in action_json.items() if k not in ["action", "reason"]} - - target_message = None - if action not in ["no_action", "no_reply", "do_nothing", "proactive_reply"]: - if target_message_id := action_json.get("target_message_id"): - target_message = self.find_message_by_id(target_message_id, message_id_list) - if target_message is None: - logger.warning(f"{self.log_prefix}无法找到target_message_id '{target_message_id}'") - target_message = self.get_latest_message(message_id_list) - else: - logger.warning(f"{self.log_prefix}动作'{action}'缺少target_message_id") - - available_action_names = [name for name, _ in current_available_actions] - if action not in ["no_action", "no_reply", "reply", "do_nothing", "proactive_reply"] and action not in available_action_names: - logger.warning( - f"{self.log_prefix}LLM 返回了当前不可用或无效的动作: '{action}' (可用: {available_action_names}),将强制使用 'no_action'" - ) - reasoning = f"LLM 返回了当前不可用的动作 '{action}' (可用: {available_action_names})。原始理由: {reasoning}" - action = "no_action" - - # 将列表转换为字典格式以供将来使用 - available_actions_dict = dict(current_available_actions) - parsed_actions.append( - { - "action_type": action, - "reasoning": reasoning, - "action_data": action_data, - "action_message": target_message, - "available_actions": available_actions_dict, - } - ) - # 如果是at_user动作且只有user_name,尝试转换为user_id - if action == "at_user" and "user_name" in action_data and "user_id" not in action_data: - user_name = action_data["user_name"] - from src.person_info.person_info import get_person_info_manager - user_info = await get_person_info_manager().get_person_info_by_name(user_name) - if user_info and user_info.get("user_id"): - action_data["user_id"] = user_info["user_id"] - logger.info(f"成功将用户名 '{user_name}' 解析为 user_id '{user_info['user_id']}'") - else: - logger.warning(f"无法将用户名 '{user_name}' 解析为 user_id") - except Exception as e: - logger.error(f"{self.log_prefix}解析单个action时出错: {e}") - parsed_actions.append( - { - "action_type": "no_action", - "reasoning": f"解析action时出错: {e}", - "action_data": {}, - "action_message": None, - "available_actions": dict(current_available_actions), - } - ) - return parsed_actions - - def _filter_no_actions(self, action_list: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """ - [注释] 从一个action字典列表中过滤掉所有的 'no_action'。 - 如果过滤后列表为空, 则返回一个空的列表, 或者根据需要返回一个默认的no_action字典。 - """ - non_no_actions = [a for a in action_list if a.get("action_type") not in ["no_action", "no_reply"]] - if non_no_actions: - return non_no_actions - # 如果都是 no_action,则返回一个包含第一个 no_action 的列表,以保留 reason - return action_list[:1] if action_list else [] - + self.generator = PlanGenerator(chat_id) + self.filter = PlanFilter() + self.executor = PlanExecutor(action_manager) async def plan( - self, - mode: ChatMode = ChatMode.FOCUS, - loop_start_time: float = 0.0, - available_actions: Optional[Dict[str, ActionInfo]] = None, - pseudo_message: Optional[str] = None, - ) -> Tuple[List[Dict[str, Any]], Optional[Dict[str, Any]]]: + self, mode: ChatMode = ChatMode.FOCUS + ) -> Tuple[List[Dict], Optional[Dict]]: """ - [注释] "大脑"规划器。 - 统一决策是否进行聊天回复(reply)以及执行哪些actions。 + 执行完整的规划流程。 """ - # --- 1. 准备上下文信息 --- - is_group_chat, chat_target_info, current_available_actions = self.get_necessary_info() - if available_actions is None: - available_actions = current_available_actions + # 1. 生成初始 Plan + initial_plan = await self.generator.generate(mode) - # --- 2. 大脑统一决策 --- - final_actions: List[Dict[str, Any]] = [] - try: - prompt, used_message_id_list = await self.build_planner_prompt( - is_group_chat=is_group_chat, - chat_target_info=chat_target_info, - current_available_actions=available_actions, - mode=mode, - ) - llm_content, _ = await self.planner_llm.generate_response_async(prompt=prompt) + # 2. 筛选 Plan + filtered_plan = await self.filter.filter(initial_plan) - if llm_content: - parsed_json = orjson.loads(repair_json(llm_content)) - - # 确保处理的是列表 - if isinstance(parsed_json, dict): - parsed_json = [parsed_json] + # 3. 执行 Plan + await self.executor.execute(filtered_plan) - if isinstance(parsed_json, list): - for item in parsed_json: - if isinstance(item, dict): - final_actions.extend(await self._parse_single_action(item, used_message_id_list, list(available_actions.items()))) - - # 如果是私聊且开启了强制回复,并且没有任何回复性action,则强制添加reply - if not is_group_chat and global_config.chat.force_reply_private: - has_reply_action = any(a.get("action_type") == "reply" for a in final_actions) - if not has_reply_action: - final_actions.append({ - "action_type": "reply", - "reasoning": "私聊强制回复", - "action_data": {}, - "action_message": self.get_latest_message(used_message_id_list), - "available_actions": available_actions, - }) - logger.info(f"{self.log_prefix}私聊强制回复已触发,添加 'reply' 动作") - - logger.info(f"{self.log_prefix}大脑决策: {[a.get('action_type') for a in final_actions]}") - - except Exception as e: - logger.error(f"{self.log_prefix}大脑处理过程中发生意外错误: {e}\n{traceback.format_exc()}") - final_actions.append({"action_type": "no_action", "reasoning": f"大脑处理错误: {e}"}) - - # --- 3. 后处理 --- - final_actions = self._filter_no_actions(final_actions) - - # === 概率模式后处理:根据配置决定是否强制添加 emoji 动作 === - if global_config.emoji.emoji_activate_type == 'random': - has_reply_action = any(a.get("action_type") == "reply" for a in final_actions) - if has_reply_action: - # 检查此动作是否已被选择 - is_already_chosen = any(a.get("action_type") == 'emoji' for a in final_actions) - if not is_already_chosen: - if random.random() < global_config.emoji.emoji_chance: - logger.info(f"{self.log_prefix}根据概率 '{global_config.emoji.emoji_chance}' 添加 emoji 动作") - final_actions.append({ - "action_type": 'emoji', - "reasoning": f"根据概率 {global_config.emoji.emoji_chance} 自动添加", - "action_data": {}, - "action_message": self.get_latest_message(used_message_id_list), - "available_actions": available_actions, - }) - - if not final_actions: - final_actions = [ - { - "action_type": "no_action", - "reasoning": "规划器选择不执行动作", - "action_data": {}, "action_message": None, "available_actions": available_actions - } - ] - - final_target_message = next((act.get("action_message") for act in final_actions if act.get("action_message")), None) - - # 记录每个动作的原因 - for action_info in final_actions: - action_type = action_info.get("action_type", "N/A") - reasoning = action_info.get("reasoning", "无") - logger.info(f"{self.log_prefix}决策: [{action_type}],原因: {reasoning}") - - actions_str = ", ".join([a.get('action_type', 'N/A') for a in final_actions]) - logger.info(f"{self.log_prefix}最终执行动作 ({len(final_actions)}): [{actions_str}]") + # 4. 返回结果 (与旧版 planner 的返回值保持兼容) + final_actions = filtered_plan.decided_actions or [] + final_target_message = next( + (act.action_message for act in final_actions if act.action_message), None + ) - return final_actions, final_target_message + final_actions_dict = [asdict(act) for act in final_actions] + final_target_message_dict = asdict(final_target_message) if final_target_message else None - async def build_planner_prompt( - self, - is_group_chat: bool, - chat_target_info: Optional[dict], - current_available_actions: Dict[str, ActionInfo], - mode: ChatMode = ChatMode.FOCUS, - refresh_time: bool = False, # 添加缺失的参数 - ) -> tuple[str, list]: - """构建 Planner LLM 的提示词 (获取模板并填充数据)""" - try: - # --- 通用信息获取 --- - time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - bot_name = global_config.bot.nickname - bot_nickname = ( - f",也有人叫你{','.join(global_config.bot.alias_names)}" if global_config.bot.alias_names else "" - ) - bot_core_personality = global_config.personality.personality_core - identity_block = f"你的名字是{bot_name}{bot_nickname},你{bot_core_personality}:" - - schedule_block = "" - if global_config.planning_system.schedule_enable: - if current_activity := schedule_manager.get_current_activity(): - schedule_block = f"你当前正在:{current_activity},但注意它与群聊的聊天无关。" - - mood_block = "" - if global_config.mood.enable_mood: - chat_mood = mood_manager.get_mood_by_chat_id(self.chat_id) - mood_block = f"你现在的心情是:{chat_mood.mood_state}" - - # --- 根据模式构建不同的Prompt --- - if mode == ChatMode.PROACTIVE: - long_term_memory_block = await self._get_long_term_memory_context() - - # 获取最近的聊天记录用于主动思考决策 - message_list_short = get_raw_msg_before_timestamp_with_chat( - chat_id=self.chat_id, - timestamp=time.time(), - limit=int(global_config.chat.max_context_size * 0.2), # 主动思考时只看少量最近消息 - ) - chat_content_block, message_id_list = build_readable_messages_with_id( - messages=message_list_short, - timestamp_mode="normal", - truncate=False, - show_actions=False, - ) - - prompt_template = await global_prompt_manager.get_prompt_async("proactive_planner_prompt") - actions_before_now = get_actions_by_timestamp_with_chat( - chat_id=self.chat_id, - timestamp_start=time.time() - 3600, - timestamp_end=time.time(), - limit=5, - ) - actions_before_now_block = build_readable_actions(actions=actions_before_now) - actions_before_now_block = f"你刚刚选择并执行过的action是:\n{actions_before_now_block}" - - prompt = prompt_template.format( - time_block=time_block, - identity_block=identity_block, - schedule_block=schedule_block, - mood_block=mood_block, - long_term_memory_block=long_term_memory_block, - chat_content_block=chat_content_block or "最近没有聊天内容。", - actions_before_now_block=actions_before_now_block, - ) - return prompt, message_id_list - - # --- FOCUS 和 NORMAL 模式的逻辑 --- - message_list_before_now = get_raw_msg_before_timestamp_with_chat( - chat_id=self.chat_id, - timestamp=time.time(), - limit=int(global_config.chat.max_context_size * 0.6), - ) - chat_content_block, message_id_list = build_readable_messages_with_id( - messages=message_list_before_now, - timestamp_mode="normal", - read_mark=self.last_obs_time_mark, - truncate=True, - show_actions=True, - ) - - actions_before_now = get_actions_by_timestamp_with_chat( - chat_id=self.chat_id, - timestamp_start=time.time() - 3600, - timestamp_end=time.time(), - limit=5, - ) - - actions_before_now_block = build_readable_actions(actions=actions_before_now) - actions_before_now_block = f"你刚刚选择并执行过的action是:\n{actions_before_now_block}" - - if refresh_time: - self.last_obs_time_mark = time.time() - - mentioned_bonus = "" - if global_config.chat.mentioned_bot_inevitable_reply: - mentioned_bonus = "\n- 有人提到你" - if global_config.chat.at_bot_inevitable_reply: - mentioned_bonus = "\n- 有人提到你,或者at你" - - if mode == ChatMode.FOCUS: - no_action_block = """ -动作:no_action -动作描述:不选择任何动作 -{{ - "action": "no_action", - "reason":"不动作的原因" -}} - -动作:no_reply -动作描述:不进行回复,等待合适的回复时机 -- 当你刚刚发送了消息,没有人回复时,选择no_reply -- 当你一次发送了太多消息,为了避免打扰聊天节奏,选择no_reply -{{ - "action": "no_reply", - "reason":"不回复的原因" -}} -""" - else: # NORMAL Mode - no_action_block = """重要说明: -- 'reply' 表示只进行普通聊天回复,不执行任何额外动作 -- 其他action表示在普通回复的基础上,执行相应的额外动作 -{{ - "action": "reply", - "target_message_id":"触发action的消息id", - "reason":"回复的原因" -}}""" - - chat_context_description = "你现在正在一个群聊中" - chat_target_name = None - if not is_group_chat and chat_target_info: - chat_target_name = ( - chat_target_info.get("person_name") or chat_target_info.get("user_nickname") or "对方" - ) - chat_context_description = f"你正在和 {chat_target_name} 私聊" - - action_options_block = await self._build_action_options(current_available_actions, mode) - - moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。" - - custom_prompt_block = "" - if global_config.custom_prompt.planner_custom_prompt_content: - custom_prompt_block = global_config.custom_prompt.planner_custom_prompt_content - - from src.person_info.person_info import get_person_info_manager - users_in_chat_str = "" - if is_group_chat and chat_target_info and chat_target_info.get("group_id"): - user_list = await get_person_info_manager().get_specific_value_list("person_name", lambda x: x is not None) - if user_list: - users_in_chat_str = "当前聊天中的用户列表(用于@):\n" + "\n".join([f"- {name} (ID: {pid})" for pid, name in user_list.items()]) + "\n" - - - planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt") - prompt = planner_prompt_template.format( - schedule_block=schedule_block, - mood_block=mood_block, - time_block=time_block, - chat_context_description=chat_context_description, - chat_content_block=chat_content_block, - actions_before_now_block=actions_before_now_block, - mentioned_bonus=mentioned_bonus, - no_action_block=no_action_block, - action_options_text=action_options_block, - moderation_prompt=moderation_prompt_block, - identity_block=identity_block, - custom_prompt_block=custom_prompt_block, - bot_name=bot_name, - users_in_chat=users_in_chat_str - ) - return prompt, message_id_list - except Exception as e: - logger.error(f"构建 Planner 提示词时出错: {e}") - logger.error(traceback.format_exc()) - return "构建 Planner Prompt 时出错", [] - - def get_necessary_info(self) -> Tuple[bool, Optional[dict], Dict[str, ActionInfo]]: - """ - 获取 Planner 需要的必要信息 - """ - is_group_chat = True - is_group_chat, chat_target_info = get_chat_type_and_target_info(self.chat_id) - logger.debug(f"{self.log_prefix}获取到聊天信息 - 群聊: {is_group_chat}, 目标信息: {chat_target_info}") - - current_available_actions_dict = self.action_manager.get_using_actions() - - # 获取完整的动作信息 - all_registered_actions: Dict[str, ActionInfo] = component_registry.get_components_by_type( # type: ignore - ComponentType.ACTION - ) - current_available_actions = {} - for action_name in current_available_actions_dict: - if action_name in all_registered_actions: - current_available_actions[action_name] = all_registered_actions[action_name] - else: - logger.warning(f"{self.log_prefix}使用中的动作 {action_name} 未在已注册动作中找到") - - # 将no_reply作为系统级特殊动作添加到可用动作中 - # no_reply虽然是系统级决策,但需要让规划器认为它是可用的 - no_reply_info = ActionInfo( - name="no_reply", - component_type=ComponentType.ACTION, - description="系统级动作:选择不回复消息的决策", - action_parameters={}, - activation_keywords=[], - plugin_name="SYSTEM", - enabled=True, # 始终启用 - parallel_action=False, - ) - current_available_actions["no_reply"] = no_reply_info - - return is_group_chat, chat_target_info, current_available_actions - - -init_prompt() + return final_actions_dict, final_target_message_dict diff --git a/src/chat/planner_actions/planner_prompts.py b/src/chat/planner_actions/planner_prompts.py new file mode 100644 index 000000000..1bfd21098 --- /dev/null +++ b/src/chat/planner_actions/planner_prompts.py @@ -0,0 +1,158 @@ +""" +本文件集中管理所有与规划器相关的提示词模板。 +""" +from src.chat.utils.prompt import Prompt + +def init_prompts(): + """ + 初始化并注册所有规划器相关的提示词。 + """ + Prompt( + """ +{schedule_block} +{mood_block} +{time_block} +{identity_block} + +{users_in_chat} +{custom_prompt_block} +{chat_context_description},以下是具体的聊天内容。 +{chat_content_block} + +{moderation_prompt} + +**任务: 构建一个完整的响应** +你的任务是根据当前的聊天内容,构建一个完整的、人性化的响应。一个完整的响应由两部分组成: +1. **主要动作**: 这是响应的核心,通常是 `reply`(文本回复)。 +2. **辅助动作 (可选)**: 这是为了增强表达效果的附加动作,例如 `emoji`(发送表情包)或 `poke_user`(戳一戳)。 + +**决策流程:** +1. 首先,决定是否要进行 `reply`。 +2. 然后,评估当前的对话气氛和用户情绪,判断是否需要一个**辅助动作**来让你的回应更生动、更符合你的性格。 +3. 如果需要,选择一个最合适的辅助动作与 `reply` 组合。 +4. 如果用户明确要求了某个动作,请务必优先满足。 + +**可用动作:** +{actions_before_now_block} + +{no_action_block} + +动作:reply +动作描述:参与聊天回复,发送文本进行表达 +- 你想要闲聊或者随便附和 +- {mentioned_bonus} +- 如果你刚刚进行了回复,不要对同一个话题重复回应 +- 不要回复自己发送的消息 +{{ + "action": "reply", + "target_message_id": "触发action的消息id", + "reason": "回复的原因" +}} + +{action_options_text} + + +**输出格式:** +你必须以严格的 JSON 格式输出,返回一个包含所有选定动作的JSON列表。如果没有任何合适的动作,返回一个空列表[]。 + +**单动作示例 (仅回复):** +[ + {{ + "action": "reply", + "target_message_id": "m123", + "reason": "回答用户的问题" + }} +] + +**组合动作示例 (回复 + 表情包):** +[ + {{ + "action": "reply", + "target_message_id": "m123", + "reason": "回答用户的问题" + }}, + {{ + "action": "emoji", + "target_message_id": "m123", + "reason": "用一个可爱的表情来缓和气氛" + }} +] + +不要输出markdown格式```json等内容,直接输出且仅包含 JSON 列表内容: +""", + "planner_prompt", + ) + + Prompt( + """ +# 主动思考决策 + +## 你的内部状态 +{time_block} +{identity_block} +{schedule_block} +{mood_block} + +## 长期记忆摘要 +{long_term_memory_block} + +## 最近的聊天内容 +{chat_content_block} + +## 最近的动作历史 +{actions_before_now_block} + +## 任务 +你现在要决定是否主动说些什么。就像一个真实的人一样,有时候会突然想起之前聊到的话题,或者对朋友的近况感到好奇,想主动询问或关心一下。 + +请基于聊天内容,用你的判断力来决定是否要主动发言。不要按照固定规则,而是像人类一样自然地思考: +- 是否想起了什么之前提到的事情,想问问后来怎么样了? +- 是否注意到朋友提到了什么值得关心的事情? +- 是否有什么话题突然想到,觉得现在聊聊很合适? +- 或者觉得现在保持沉默更好? + +## 可用动作 +动作:proactive_reply +动作描述:主动发起对话,可以是关心朋友、询问近况、延续之前的话题,或分享想法。 +- 当你突然想起之前的话题,想询问进展时 +- 当你想关心朋友的情况时 +- 当你有什么想法想分享时 +- 当你觉得现在是个合适的聊天时机时 +{{ + "action": "proactive_reply", + "reason": "你决定主动发言的具体原因", + "topic": "你想说的内容主题(简洁描述)" +}} + +动作:do_nothing +动作描述:保持沉默,不主动发起对话。 +- 当你觉得现在不是合适的时机时 +- 当最近已经说得够多了时 +- 当对话氛围不适合插入时 +{{ + "action": "do_nothing", + "reason": "决定保持沉默的原因" +}} + +你必须从上面列出的可用action中选择一个。要像真人一样自然地思考和决策。 +请以严格的 JSON 格式输出,且仅包含 JSON 内容: +""", + "proactive_planner_prompt", + ) + + Prompt( + """ +动作:{action_name} +动作描述:{action_description} +{action_require} +{{ + "action": "{action_name}", + "target_message_id": "触发action的消息id", + "reason": "触发action的原因"{action_parameters} +}} +""", + "action_prompt", + ) + +# 在模块加载时自动初始化 +init_prompts() \ No newline at end of file diff --git a/src/common/data_models/info_data_model.py b/src/common/data_models/info_data_model.py index 0f7b1f950..2806587c1 100644 --- a/src/common/data_models/info_data_model.py +++ b/src/common/data_models/info_data_model.py @@ -1,10 +1,10 @@ from dataclasses import dataclass, field -from typing import Optional, Dict, TYPE_CHECKING +from typing import Optional, Dict, List, TYPE_CHECKING from . import BaseDataModel if TYPE_CHECKING: from .database_data_model import DatabaseMessages - from src.plugin_system.base.component_types import ActionInfo + from src.plugin_system.base.component_types import ActionInfo, ChatMode @dataclass @@ -23,3 +23,21 @@ class ActionPlannerInfo(BaseDataModel): action_data: Optional[Dict] = None action_message: Optional["DatabaseMessages"] = None available_actions: Optional[Dict[str, "ActionInfo"]] = None + + +@dataclass +class Plan(BaseDataModel): + """ + 统一规划数据模型 + """ + chat_id: str + mode: "ChatMode" + + # Generator 填充 + available_actions: Dict[str, "ActionInfo"] = field(default_factory=dict) + chat_history: List["DatabaseMessages"] = field(default_factory=list) + target_info: Optional[TargetPersonInfo] = None + + # Filter 填充 + llm_prompt: Optional[str] = None + decided_actions: Optional[List[ActionPlannerInfo]] = None From 07f37323b278dec7f244d53e08df534b7e45d068 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 13 Sep 2025 11:57:05 +0800 Subject: [PATCH 22/40] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86**AI=E5=86=99?= =?UTF-8?q?=E5=87=BA=E6=9D=A5=E7=9A=84=E8=99=AB=E5=AD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat_loop/cycle_processor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index 4bb9dddcd..2e290ae9d 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -360,7 +360,7 @@ class CycleProcessor: self.context.chat_instance.cycle_tracker.end_cycle(loop_info, cycle_timers) self.context.chat_instance.cycle_tracker.print_cycle_info(cycle_timers) - action_type = actions["action_type"] if actions else "no_action" + action_type = actions[0]["action_type"] if actions else "no_action" return action_type async def _handle_action( @@ -411,7 +411,7 @@ class CycleProcessor: if "reply" in available_actions: fallback_action = "reply" elif available_actions: - fallback_action = list(available_actions.keys()) + fallback_action = list(available_actions.keys())[0] if fallback_action and fallback_action != action: logger.info(f"{self.context.log_prefix} 使用回退动作: {fallback_action}") From f1c1a2081af00c7d1dab9601189ce42ec71fb60a Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 13 Sep 2025 12:04:34 +0800 Subject: [PATCH 23/40] =?UTF-8?q?docs(planner):=20=E4=B8=BA=E8=A7=84?= =?UTF-8?q?=E5=88=92=E5=99=A8=E7=9B=B8=E5=85=B3=E6=A8=A1=E5=9D=97=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E8=AF=A6=E7=BB=86=E7=9A=84=E6=96=87=E6=A1=A3=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E4=B8=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 `plan_executor`、`planner` 和 `planner_prompts` 模块中的类和函数补充了详细的文档字符串(docstrings)。 这提高了代码的可读性和可维护性,阐明了每个组件的职责、参数和返回值,使得其他开发者能更容易地理解和使用这些模块。 --- src/chat/planner_actions/plan_executor.py | 21 +++++++++++-- src/chat/planner_actions/plan_generator.py | 35 +++++++++++++++++++-- src/chat/planner_actions/planner.py | 34 ++++++++++++++++++-- src/chat/planner_actions/planner_prompts.py | 20 ++++++++++-- 4 files changed, 100 insertions(+), 10 deletions(-) diff --git a/src/chat/planner_actions/plan_executor.py b/src/chat/planner_actions/plan_executor.py index 6cc828a64..b27ef12e3 100644 --- a/src/chat/planner_actions/plan_executor.py +++ b/src/chat/planner_actions/plan_executor.py @@ -10,15 +10,32 @@ logger = get_logger("plan_executor") class PlanExecutor: """ - 执行 Plan 中最终确定的动作。 + 负责接收一个 Plan 对象,并执行其中最终确定的所有动作。 + + 这个类是规划流程的最后一步,将规划结果转化为实际的动作执行。 + + Attributes: + action_manager (ActionManager): 用于实际执行各种动作的管理器实例。 """ def __init__(self, action_manager: ActionManager): + """ + 初始化 PlanExecutor。 + + Args: + action_manager (ActionManager): 一个 ActionManager 实例,用于执行动作。 + """ self.action_manager = action_manager async def execute(self, plan: Plan): """ - 读取 Plan 对象的 decided_actions 字段并执行。 + 遍历并执行 Plan 对象中 `decided_actions` 列表里的所有动作。 + + 如果动作类型为 "no_action",则会记录原因并跳过。 + 否则,它将调用 ActionManager 来执行相应的动作。 + + Args: + plan (Plan): 包含待执行动作列表的 Plan 对象。 """ if not plan.decided_actions: logger.info("没有需要执行的动作。") diff --git a/src/chat/planner_actions/plan_generator.py b/src/chat/planner_actions/plan_generator.py index e3b08eff1..f3537b19b 100644 --- a/src/chat/planner_actions/plan_generator.py +++ b/src/chat/planner_actions/plan_generator.py @@ -15,10 +15,25 @@ from src.plugin_system.core.component_registry import component_registry class PlanGenerator: """ - 搜集信息并生成初始 Plan 对象。 + PlanGenerator 负责在规划流程的初始阶段收集所有必要信息。 + + 它会汇总以下信息来构建一个“原始”的 Plan 对象,该对象后续会由 PlanFilter 进行筛选: + - 当前聊天信息 (ID, 目标用户) + - 当前可用的动作列表 + - 最近的聊天历史记录 + + Attributes: + chat_id (str): 当前聊天的唯一标识符。 + action_manager (ActionManager): 用于获取可用动作列表的管理器。 """ def __init__(self, chat_id: str): + """ + 初始化 PlanGenerator。 + + Args: + chat_id (str): 当前聊天的 ID。 + """ from src.chat.planner_actions.action_manager import ActionManager self.chat_id = chat_id # 注意:ActionManager 可能需要根据实际情况初始化 @@ -26,7 +41,15 @@ class PlanGenerator: async def generate(self, mode: ChatMode) -> Plan: """ - 生成并填充初始的 Plan 对象。 + 收集所有信息,生成并返回一个初始的 Plan 对象。 + + 这个 Plan 对象包含了决策所需的所有上下文信息。 + + Args: + mode (ChatMode): 当前的聊天模式。 + + Returns: + Plan: 一个填充了初始上下文信息的 Plan 对象。 """ _is_group_chat, chat_target_info_dict = get_chat_type_and_target_info(self.chat_id) @@ -55,7 +78,13 @@ class PlanGenerator: def _get_available_actions(self) -> Dict[str, "ActionInfo"]: """ - 获取当前可用的动作。 + 从 ActionManager 和组件注册表中获取当前所有可用的动作。 + + 它会合并已注册的动作和系统级动作(如 "no_reply"), + 并以字典形式返回。 + + Returns: + Dict[str, "ActionInfo"]: 一个字典,键是动作名称,值是 ActionInfo 对象。 """ current_available_actions_dict = self.action_manager.get_using_actions() all_registered_actions: Dict[str, ActionInfo] = component_registry.get_components_by_type( # type: ignore diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 52b879108..33bfe2a72 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -20,10 +20,29 @@ logger = get_logger("planner") class ActionPlanner: """ - 协调器,按顺序调用 Generator -> Filter -> Executor。 + ActionPlanner 是规划系统的核心协调器。 + + 它负责整合规划流程的三个主要阶段: + 1. **生成 (Generate)**: 使用 PlanGenerator 创建一个初始的行动计划。 + 2. **筛选 (Filter)**: 使用 PlanFilter 对生成的计划进行审查和优化。 + 3. **执行 (Execute)**: 使用 PlanExecutor 执行最终确定的行动。 + + Attributes: + chat_id (str): 当前聊天的唯一标识符。 + action_manager (ActionManager): 用于执行具体动作的管理器。 + generator (PlanGenerator): 负责生成初始计划。 + filter (PlanFilter): 负责筛选和优化计划。 + executor (PlanExecutor): 负责执行最终计划。 """ def __init__(self, chat_id: str, action_manager: ActionManager): + """ + 初始化 ActionPlanner。 + + Args: + chat_id (str): 当前聊天的 ID。 + action_manager (ActionManager): 一个 ActionManager 实例。 + """ self.chat_id = chat_id self.action_manager = action_manager self.generator = PlanGenerator(chat_id) @@ -34,7 +53,18 @@ class ActionPlanner: self, mode: ChatMode = ChatMode.FOCUS ) -> Tuple[List[Dict], Optional[Dict]]: """ - 执行完整的规划流程。 + 执行从生成到执行的完整规划流程。 + + 这个方法按顺序协调生成、筛选和执行三个阶段。 + + Args: + mode (ChatMode): 当前的聊天模式,默认为 FOCUS。 + + Returns: + Tuple[List[Dict], Optional[Dict]]: 一个元组,包含: + - final_actions_dict (List[Dict]): 最终确定的动作列表(字典格式)。 + - final_target_message_dict (Optional[Dict]): 最终的目标消息(字典格式),如果没有则为 None。 + 这与旧版 planner 的返回值保持兼容。 """ # 1. 生成初始 Plan initial_plan = await self.generator.generate(mode) diff --git a/src/chat/planner_actions/planner_prompts.py b/src/chat/planner_actions/planner_prompts.py index 1bfd21098..4aa4bfc64 100644 --- a/src/chat/planner_actions/planner_prompts.py +++ b/src/chat/planner_actions/planner_prompts.py @@ -1,12 +1,21 @@ """ -本文件集中管理所有与规划器相关的提示词模板。 +本文件集中管理所有与规划器(Planner)相关的提示词(Prompt)模板。 + +通过将提示词与代码逻辑分离,可以更方便地对模型的行为进行迭代和优化, +而无需修改核心代码。 """ from src.chat.utils.prompt import Prompt + def init_prompts(): """ - 初始化并注册所有规划器相关的提示词。 + 初始化并向 Prompt 注册系统注册所有规划器相关的提示词。 + + 这个函数会在模块加载时自动调用,确保所有提示词在系统启动时都已准备就绪。 """ + # 核心规划器提示词,用于在接收到新消息时决定如何回应。 + # 它构建了一个复杂的上下文,包括历史记录、可用动作、角色设定等, + # 并要求模型以 JSON 格式输出一个或多个动作组合。 Prompt( """ {schedule_block} @@ -83,6 +92,8 @@ def init_prompts(): "planner_prompt", ) + # 主动思考规划器提示词,用于在没有新消息时决定是否要主动发起对话。 + # 它模拟了人类的自发性思考,允许模型根据长期记忆和最近的对话来决定是否开启新话题。 Prompt( """ # 主动思考决策 @@ -140,6 +151,8 @@ def init_prompts(): "proactive_planner_prompt", ) + # 单个动作的格式化提示词模板。 + # 用于将每个可用动作的信息格式化后,插入到主提示词的 {action_options_text} 占位符中。 Prompt( """ 动作:{action_name} @@ -154,5 +167,6 @@ def init_prompts(): "action_prompt", ) -# 在模块加载时自动初始化 + +# 在模块加载时自动调用,完成提示词的注册。 init_prompts() \ No newline at end of file From 490bf9e7ccc7a5935c2e772df9d2707cece55207 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 13 Sep 2025 12:07:19 +0800 Subject: [PATCH 24/40] =?UTF-8?q?chore(planner):=20=E4=B8=B4=E6=97=B6?= =?UTF-8?q?=E7=A6=81=E7=94=A8=E6=89=A7=E8=A1=8C=E5=99=A8=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为了调试规划器流程的其他部分,暂时注释掉了对 `self.executor.execute(filtered_plan)` 的调用。 --- src/chat/planner_actions/planner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 33bfe2a72..b69f3a2ba 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -72,8 +72,8 @@ class ActionPlanner: # 2. 筛选 Plan filtered_plan = await self.filter.filter(initial_plan) - # 3. 执行 Plan - await self.executor.execute(filtered_plan) + # 3. 执行 Plan(临时引爆因为它暂时还跑不了) + #await self.executor.execute(filtered_plan) # 4. 返回结果 (与旧版 planner 的返回值保持兼容) final_actions = filtered_plan.decided_actions or [] From 8fcb49a1bb0e25425d26f5a1ae293050df0b5055 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 13 Sep 2025 12:19:15 +0800 Subject: [PATCH 25/40] =?UTF-8?q?=E8=AF=95=E5=9B=BE=E5=BC=95=E7=88=86?= =?UTF-8?q?=E5=AE=9A=E6=97=B6=E6=8F=90=E9=86=92,=E6=88=91=E6=9C=89?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E6=9B=B4=E5=A5=BD=E7=9A=84=E4=B8=9C=E8=A5=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat_loop/proactive/event_scheduler.py | 242 ---------------- .../chat_loop/proactive/proactive_thinker.py | 4 +- .../proactive/smart_reminder_analyzer.py | 260 ------------------ 3 files changed, 2 insertions(+), 504 deletions(-) delete mode 100644 src/chat/chat_loop/proactive/event_scheduler.py delete mode 100644 src/chat/chat_loop/proactive/smart_reminder_analyzer.py diff --git a/src/chat/chat_loop/proactive/event_scheduler.py b/src/chat/chat_loop/proactive/event_scheduler.py deleted file mode 100644 index 8c005cb3d..000000000 --- a/src/chat/chat_loop/proactive/event_scheduler.py +++ /dev/null @@ -1,242 +0,0 @@ -""" -事件驱动的智能调度器 -基于asyncio的精确定时事件调度系统,替代轮询机制 -""" - -import asyncio -import traceback -from datetime import datetime -from typing import Dict, Callable, Any, Optional -from dataclasses import dataclass -from src.common.logger import get_logger - -logger = get_logger("event_scheduler") - - -@dataclass -class ScheduledEvent: - """调度事件数据类""" - event_id: str - trigger_time: datetime - callback: Callable - metadata: Dict[str, Any] - task: Optional[asyncio.Task] = None - - -class EventDrivenScheduler: - """事件驱动的调度器""" - - def __init__(self): - self.scheduled_events: Dict[str, ScheduledEvent] = {} - self._shutdown = False - - async def schedule_event( - self, - event_id: str, - trigger_time: datetime, - callback: Callable, - metadata: Dict[str, Any] = None - ) -> bool: - """ - 调度一个事件在指定时间触发 - - Args: - event_id: 事件唯一标识 - trigger_time: 触发时间 - callback: 回调函数 - metadata: 事件元数据 - - Returns: - bool: 调度成功返回True - """ - try: - if metadata is None: - metadata = {} - - # 如果事件已存在,先取消 - if event_id in self.scheduled_events: - await self.cancel_event(event_id) - - # 计算延迟时间 - now = datetime.now() - delay = (trigger_time - now).total_seconds() - - if delay <= 0: - logger.warning(f"事件 {event_id} 的触发时间已过,立即执行") - # 立即执行 - asyncio.create_task(self._execute_callback(event_id, callback, metadata)) - return True - - # 创建调度事件 - scheduled_event = ScheduledEvent( - event_id=event_id, - trigger_time=trigger_time, - callback=callback, - metadata=metadata - ) - - # 创建异步任务 - scheduled_event.task = asyncio.create_task( - self._wait_and_execute(scheduled_event) - ) - - self.scheduled_events[event_id] = scheduled_event - logger.info(f"调度事件 {event_id} 将在 {trigger_time} 触发 (延迟 {delay:.1f} 秒)") - return True - - except Exception as e: - logger.error(f"调度事件失败: {e}") - return False - - async def _wait_and_execute(self, event: ScheduledEvent): - """等待并执行事件""" - try: - now = datetime.now() - delay = (event.trigger_time - now).total_seconds() - - if delay > 0: - await asyncio.sleep(delay) - - # 检查是否被取消 - if self._shutdown or event.event_id not in self.scheduled_events: - return - - # 执行回调 - await self._execute_callback(event.event_id, event.callback, event.metadata) - - except asyncio.CancelledError: - logger.info(f"事件 {event.event_id} 被取消") - except Exception as e: - logger.error(f"执行事件 {event.event_id} 时出错: {e}") - finally: - # 清理已完成的事件 - if event.event_id in self.scheduled_events: - del self.scheduled_events[event.event_id] - - async def _execute_callback(self, event_id: str, callback: Callable, metadata: Dict[str, Any]): - """执行回调函数""" - try: - logger.info(f"执行调度事件: {event_id}") - - # 根据回调函数签名调用 - if asyncio.iscoroutinefunction(callback): - await callback(metadata) - else: - callback(metadata) - - except Exception as e: - logger.error(f"执行回调函数失败: {e}") - logger.error(traceback.format_exc()) - - async def cancel_event(self, event_id: str) -> bool: - """ - 取消一个调度事件 - - Args: - event_id: 事件ID - - Returns: - bool: 取消成功返回True - """ - try: - if event_id in self.scheduled_events: - event = self.scheduled_events[event_id] - if event.task and not event.task.done(): - event.task.cancel() - del self.scheduled_events[event_id] - logger.info(f"取消调度事件: {event_id}") - return True - return False - except Exception as e: - logger.error(f"取消事件失败: {e}") - return False - - async def shutdown(self): - """关闭调度器,取消所有事件""" - self._shutdown = True - for event_id in list(self.scheduled_events.keys()): - await self.cancel_event(event_id) - logger.info("事件调度器已关闭") - - def get_scheduled_events(self) -> Dict[str, ScheduledEvent]: - """获取所有调度事件""" - return self.scheduled_events.copy() - - def get_event_count(self) -> int: - """获取调度事件数量""" - return len(self.scheduled_events) - - -# 全局事件调度器实例 -event_scheduler = EventDrivenScheduler() - - -# 便捷函数 -async def schedule_reminder( - reminder_id: str, - reminder_time: datetime, - chat_id: str, - reminder_content: str, - callback: Callable -): - """ - 调度提醒事件的便捷函数 - - Args: - reminder_id: 提醒唯一标识 - reminder_time: 提醒时间 - chat_id: 聊天ID - reminder_content: 提醒内容 - callback: 回调函数 - """ - metadata = { - "type": "reminder", - "chat_id": chat_id, - "content": reminder_content, - "created_at": datetime.now().isoformat() - } - - return await event_scheduler.schedule_event( - event_id=reminder_id, - trigger_time=reminder_time, - callback=callback, - metadata=metadata - ) - - -async def _execute_reminder_callback(subheartflow_id: str, reminder_text: str, original_message: str = None): - """执行提醒回调函数""" - try: - # 获取对应的subheartflow实例 - from src.chat.heart_flow.heartflow import heartflow - - subflow = await heartflow.get_or_create_subheartflow(subheartflow_id) - if not subflow: - logger.error(f"无法获取subheartflow实例: {subheartflow_id}") - return - - # 创建主动思考事件,触发完整的思考流程 - from src.chat.chat_loop.proactive.events import ProactiveTriggerEvent - - # 使用原始消息来构造reason,如果没有原始消息则使用处理后的内容 - reason_content = original_message if original_message else reminder_text - - event = ProactiveTriggerEvent( - source="reminder_system", - reason=f"定时提醒:{reason_content}", # 这里传递完整的原始消息 - metadata={ - "reminder_text": reminder_text, - "original_message": original_message, - "trigger_time": datetime.now().isoformat() - } - ) - - # 通过subflow的HeartFChatting实例触发主动思考 - await subflow.heart_fc_instance.proactive_thinker.think(event) - - logger.info(f"已触发提醒的主动思考,内容: {reminder_text},没有传递那条消息吗?{original_message}") - - except Exception as e: - logger.error(f"执行提醒回调时发生错误: {e}") - import traceback - traceback.print_exc() \ No newline at end of file diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py index 8f304b406..5f0761de7 100644 --- a/src/chat/chat_loop/proactive/proactive_thinker.py +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -162,10 +162,10 @@ class ProactiveThinker: web_search_tool = tool_api.get_tool_instance("web_search") if web_search_tool: try: - search_result_dict = await web_search_tool.execute(search_query=topic, max_results=10) + search_result_dict = await web_search_tool.execute(function_args={"keyword": topic, "max_results": 10}) except TypeError: try: - search_result_dict = await web_search_tool.execute(keyword=topic, max_results=10) + search_result_dict = await web_search_tool.execute(function_args={"keyword": topic, "max_results": 10}) except TypeError: logger.warning(f"{self.context.log_prefix} 网络搜索工具参数不匹配,跳过搜索") news_block = "跳过网络搜索。" diff --git a/src/chat/chat_loop/proactive/smart_reminder_analyzer.py b/src/chat/chat_loop/proactive/smart_reminder_analyzer.py deleted file mode 100644 index 3498cb2f5..000000000 --- a/src/chat/chat_loop/proactive/smart_reminder_analyzer.py +++ /dev/null @@ -1,260 +0,0 @@ -""" -智能提醒分析器 - -使用LLM分析用户消息,识别提醒请求并提取时间和内容信息 -""" - -import re -import json -from datetime import datetime, timedelta -from typing import Optional - -from src.common.logger import get_logger -from src.llm_models.utils_model import LLMRequest -from src.config.config import model_config - -logger = get_logger("smart_reminder") - - -class ReminderEvent: - """提醒事件数据类""" - def __init__(self, user_id: str, reminder_time: datetime, content: str, confidence: float): - self.user_id = user_id - self.reminder_time = reminder_time - self.content = content - self.confidence = confidence - - def __repr__(self): - return f"ReminderEvent(user_id={self.user_id}, time={self.reminder_time}, content={self.content}, confidence={self.confidence})" - - def to_dict(self): - return { - 'user_id': self.user_id, - 'reminder_time': self.reminder_time.isoformat(), - 'content': self.content, - 'confidence': self.confidence - } - - -class SmartReminderAnalyzer: - """智能提醒分析器""" - - def __init__(self): - self.confidence_threshold = 0.7 - # 使用规划器模型进行分析 - self.analyzer_llm = LLMRequest( - model_set=model_config.model_task_config.utils_small, - request_type="reminder_analyzer" - ) - - async def analyze_message(self, user_id: str, message: str) -> Optional[ReminderEvent]: - """分析消息是否包含提醒请求 - - Args: - user_id: 用户ID - message: 用户消息内容 - - Returns: - ReminderEvent对象,如果没有检测到提醒请求则返回None - """ - if not message or len(message.strip()) == 0: - return None - - logger.debug(f"分析消息中的提醒请求: {message}") - - # 使用LLM分析消息 - analysis_result = await self._analyze_with_llm(message) - - if not analysis_result or analysis_result.get('confidence', 0) < 0.5: # 降低置信度阈值 - return None - - try: - # 解析时间 - reminder_time = self._parse_relative_time(analysis_result['relative_time']) - if not reminder_time: - return None - - # 创建提醒事件 - reminder_event = ReminderEvent( - user_id=user_id, - reminder_time=reminder_time, - content=analysis_result.get('content', '提醒'), - confidence=analysis_result['confidence'] - ) - - logger.info(f"检测到提醒请求: {reminder_event}") - return reminder_event - - except Exception as e: - logger.error(f"创建提醒事件失败: {e}") - return None - - async def _analyze_with_llm(self, message: str) -> Optional[dict]: - """使用LLM分析消息中的提醒请求""" - try: - prompt = f"""分析以下消息是否包含提醒请求。 - -消息: {message} -当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - -请判断用户是否想要设置提醒,如果是,请提取: -1. 是否包含提醒请求 (has_reminder: true/false) -2. 置信度 (confidence: 0.0-1.0) -3. 相对时间表达 (relative_time: 标准化的时间表达,例如将'半小时后'转换为'30分钟后', '明天下午三点'转换为'明天15点') -4. 提醒内容 (content: 提醒的具体内容) -5. 分析原因 (reasoning: 判断理由) - -请以JSON格式输出: -{{ - "has_reminder": true/false, - "confidence": 0.0-1.0, - "relative_time": "标准化的时间表达 (例如 '30分钟后', '2小时后')", - "content": "提醒内容", - "reasoning": "判断理由" -}}""" - - response, _ = await self.analyzer_llm.generate_response_async(prompt=prompt) - if not response: - return None - - # 解析JSON响应,处理可能的markdown包装 - try: - # 清理响应文本 - cleaned_response = response.strip() - - # 移除markdown代码块包装 - if cleaned_response.startswith('```json'): - cleaned_response = cleaned_response[7:] # 移除 ```json - elif cleaned_response.startswith('```'): - cleaned_response = cleaned_response[3:] # 移除 ``` - - if cleaned_response.endswith('```'): - cleaned_response = cleaned_response[:-3] # 移除结尾的 ``` - - cleaned_response = cleaned_response.strip() - - # 解析JSON - result = json.loads(cleaned_response) - if result.get('has_reminder', False): - logger.info(f"LLM分析结果: {result}") - return result - except json.JSONDecodeError as e: - logger.error(f"LLM响应JSON解析失败: {response}, Error: {e}") - # 尝试使用更宽松的JSON修复 - try: - import re - # 提取JSON部分的正则表达式 - json_match = re.search(r'\{.*\}', cleaned_response, re.DOTALL) - if json_match: - json_str = json_match.group() - result = json.loads(json_str) - if result.get('has_reminder', False): - logger.info(f"备用解析成功: {result}") - return result - except Exception as fallback_error: - logger.error(f"备用JSON解析也失败了: {fallback_error}") - - except Exception as e: - logger.error(f"LLM分析失败: {e}") - - return None - - def _parse_relative_time(self, time_expr: str) -> Optional[datetime]: - """解析时间表达式(支持相对时间和绝对时间)""" - try: - now = datetime.now() - - # 1. 匹配相对时间:X分钟后,包括中文数字 - # 先尝试匹配阿拉伯数字 - minutes_match = re.search(r'(\d+)\s*分钟后', time_expr) - if minutes_match: - minutes = int(minutes_match.group(1)) - result = now + timedelta(minutes=minutes) - logger.info(f"相对时间解析结果: timedelta(minutes={minutes}) -> {result}") - return result - - # 匹配中文数字分钟 - chinese_minutes_patterns = [ - (r'一分钟后', 1), (r'二分钟后', 2), (r'两分钟后', 2), (r'三分钟后', 3), (r'四分钟后', 4), (r'五分钟后', 5), - (r'六分钟后', 6), (r'七分钟后', 7), (r'八分钟后', 8), (r'九分钟后', 9), (r'十分钟后', 10), - (r'十一分钟后', 11), (r'十二分钟后', 12), (r'十三分钟后', 13), (r'十四分钟后', 14), (r'十五分钟后', 15), - (r'二十分钟后', 20), (r'三十分钟后', 30), (r'四十分钟后', 40), (r'五十分钟后', 50), (r'六十分钟后', 60) - ] - - for pattern, minutes in chinese_minutes_patterns: - if re.search(pattern, time_expr): - result = now + timedelta(minutes=minutes) - logger.info(f"中文时间解析结果: {pattern} -> {minutes}分钟 -> {result}") - return result - - # 2. 匹配相对时间:X小时后 - hours_match = re.search(r'(\d+)\s*小时后', time_expr) - if hours_match: - hours = int(hours_match.group(1)) - result = now + timedelta(hours=hours) - logger.info(f"相对时间解析结果: timedelta(hours={hours})") - return result - - # 3. 匹配相对时间:X秒后 - seconds_match = re.search(r'(\d+)\s*秒后', time_expr) - if seconds_match: - seconds = int(seconds_match.group(1)) - result = now + timedelta(seconds=seconds) - logger.info(f"相对时间解析结果: timedelta(seconds={seconds})") - return result - - # 4. 匹配明天+具体时间:明天下午2点、明天上午10点 - tomorrow_match = re.search(r'明天.*?(\d{1,2})\s*[点时]', time_expr) - if tomorrow_match: - hour = int(tomorrow_match.group(1)) - # 如果是下午且小于12,加12小时 - if '下午' in time_expr and hour < 12: - hour += 12 - elif '上午' in time_expr and hour == 12: - hour = 0 - - tomorrow = now + timedelta(days=1) - result = tomorrow.replace(hour=hour, minute=0, second=0, microsecond=0) - logger.info(f"绝对时间解析结果: 明天{hour}点") - return result - - # 5. 匹配今天+具体时间:今天下午3点、今天晚上8点 - today_match = re.search(r'今天.*?(\d{1,2})\s*[点时]', time_expr) - if today_match: - hour = int(today_match.group(1)) - # 如果是下午且小于12,加12小时 - if '下午' in time_expr and hour < 12: - hour += 12 - elif '晚上' in time_expr and hour < 12: - hour += 12 - elif '上午' in time_expr and hour == 12: - hour = 0 - - result = now.replace(hour=hour, minute=0, second=0, microsecond=0) - # 如果时间已过,设为明天 - if result <= now: - result += timedelta(days=1) - - logger.info(f"绝对时间解析结果: 今天{hour}点") - return result - - # 6. 匹配纯数字时间:14点、2点 - pure_time_match = re.search(r'(\d{1,2})\s*[点时]', time_expr) - if pure_time_match: - hour = int(pure_time_match.group(1)) - result = now.replace(hour=hour, minute=0, second=0, microsecond=0) - # 如果时间已过,设为明天 - if result <= now: - result += timedelta(days=1) - - logger.info(f"绝对时间解析结果: {hour}点") - return result - - except Exception as e: - logger.error(f"时间解析失败: {time_expr}, Error: {e}") - - return None - - -# 全局智能提醒分析器实例 -smart_reminder_analyzer = SmartReminderAnalyzer() \ No newline at end of file From 70c5c289fc1d575054bea6b5895e692194d57094 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 13 Sep 2025 12:26:07 +0800 Subject: [PATCH 26/40] =?UTF-8?q?refactor(heart=5Fflow):=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E6=99=BA=E8=83=BD=E6=8F=90=E9=86=92=E5=88=86=E6=9E=90?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 从消息处理流程中移除旧的智能提醒分析模块。 该功能已被一个新的、更可靠的提醒系统取代,因此旧的实现被移除以简化代码库并消除冗余。 --- .../heart_flow/heartflow_message_processor.py | 97 +------------------ 1 file changed, 2 insertions(+), 95 deletions(-) diff --git a/src/chat/heart_flow/heartflow_message_processor.py b/src/chat/heart_flow/heartflow_message_processor.py index a401e797b..c68df532c 100644 --- a/src/chat/heart_flow/heartflow_message_processor.py +++ b/src/chat/heart_flow/heartflow_message_processor.py @@ -117,8 +117,7 @@ class HeartFCMessageReceiver: 主要流程: 1. 消息解析与初始化 - 2. 智能提醒分析 - 3. 消息缓冲处理 + 2. 消息缓冲处理 4. 过滤检查 5. 兴趣度计算 6. 关系处理 @@ -131,99 +130,7 @@ class HeartFCMessageReceiver: userinfo = message.message_info.user_info chat = message.chat_stream - # 2. 智能提醒分析 - 检查用户是否请求提醒 - from src.chat.chat_loop.proactive.smart_reminder_analyzer import smart_reminder_analyzer - from src.chat.chat_loop.proactive.event_scheduler import event_scheduler - - try: - reminder_event = await smart_reminder_analyzer.analyze_message( - userinfo.user_id, # type: ignore - message.processed_plain_text - ) - if reminder_event: - logger.info(f"检测到提醒请求: {reminder_event}") - - # 创建提醒回调函数 - async def reminder_callback(metadata): - """提醒执行回调函数 - 触发完整的主动思考流程""" - try: - # 获取对应的subheartflow实例 - from src.chat.heart_flow.heartflow import heartflow - - subflow = await heartflow.get_or_create_subheartflow(metadata.get("chat_id")) - if not subflow: - logger.error(f"无法获取subheartflow实例: {metadata.get('chat_id')}") - return - - # 创建主动思考事件,触发完整的思考流程 - from src.chat.chat_loop.proactive.events import ProactiveTriggerEvent - - reminder_content = metadata.get('content', '提醒时间到了') - # 使用原始消息内容作为reason,如果没有则使用处理后的内容 - original_message = metadata.get('original_message', '') - reason_content = original_message if original_message else reminder_content - - event = ProactiveTriggerEvent( - source="reminder_system", - reason=f"定时提醒:{reason_content}", - metadata=metadata, - related_message_id=metadata.get("original_message_id") - ) - - # 通过subflow的HeartFChatting实例触发主动思考 - await subflow.heart_fc_instance.proactive_thinker.think(event) - - logger.info(f"已触发提醒的主动思考,内容: {reminder_content}") - - except Exception as callback_error: - logger.error(f"执行提醒回调失败: {callback_error}") - import traceback - logger.error(traceback.format_exc()) - - # Fallback: 如果主动思考失败,直接发送提醒消息 - try: - from src.plugin_system.apis.send_api import text_to_stream - reminder_content = metadata.get('content', '提醒时间到了') - await text_to_stream( - text=f"⏰ 提醒:{reminder_content}", - stream_id=metadata.get("chat_id"), - typing=False - ) - logger.info(f"Fallback提醒消息已发送: {reminder_content}") - except Exception as fallback_error: - logger.error(f"Fallback提醒也失败了: {fallback_error}") - - # 调度提醒事件 - event_id = f"reminder_{reminder_event.user_id}_{int(reminder_event.reminder_time.timestamp())}" - metadata = { - "type": "reminder", - "user_id": reminder_event.user_id, - "sender_name": userinfo.user_nickname, # 添加发送者昵称 - "platform": chat.platform, - "chat_id": chat.stream_id, - "content": reminder_event.content, - "confidence": reminder_event.confidence, - "created_at": datetime.now().isoformat(), - "original_message_id": message.message_info.message_id, - "original_message": message.processed_plain_text # 保存完整的原始消息 - } - - success = await event_scheduler.schedule_event( - event_id=event_id, - trigger_time=reminder_event.reminder_time, - callback=reminder_callback, - metadata=metadata - ) - - if success: - logger.info(f"提醒事件调度成功: {event_id}") - else: - logger.error(f"提醒事件调度失败: {event_id}") - - except Exception as e: - logger.error(f"智能提醒分析失败: {e}") - - # 3. 兴趣度计算与更新 + # 2. 兴趣度计算与更新 interested_rate, is_mentioned, keywords = await _calculate_interest(message) message.interest_value = interested_rate message.is_mentioned = is_mentioned From b9984b81f167d04564ce04c1d1856ed78e08003f Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 13 Sep 2025 12:52:00 +0800 Subject: [PATCH 27/40] =?UTF-8?q?Revert=20"feat(reminder):=20=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E5=AE=9A=E6=97=B6=E6=8F=90=E9=86=92=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=EF=BC=8C=E5=AE=9E=E7=8E=B0=E6=99=BA=E8=83=BD=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=AF=86=E5=88=AB=E4=B8=8E=E4=B8=8A=E4=B8=8B=E6=96=87=E6=84=9F?= =?UTF-8?q?=E7=9F=A5=E5=9B=9E=E5=A4=8D"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit e1ebf41f8d3ba594570bbe1f9a96880ac0492ba6. --- .../chat_loop/proactive/proactive_thinker.py | 15 +++++ src/plugins/built_in/at_user_plugin/plugin.py | 66 ++++--------------- 2 files changed, 26 insertions(+), 55 deletions(-) diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py index 5f0761de7..d63820710 100644 --- a/src/chat/chat_loop/proactive/proactive_thinker.py +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -139,6 +139,21 @@ class ProactiveThinker: logger.error(f"{self.context.log_prefix} 主动思考执行异常: {e}") logger.error(traceback.format_exc()) + async def _get_reminder_context(self, message_id: str) -> str: + """获取提醒消息的上下文""" + try: + # 只获取那一条消息 + message_record = await db_get(Messages, {"message_id": message_id}, single_result=True) + if message_record: + # 使用 build_readable_messages_with_id 来格式化单条消息 + chat_context_block, _ = build_readable_messages_with_id(messages=[message_record]) + return chat_context_block + return "无法加载相关的聊天记录。" + except Exception as e: + logger.error(f"{self.context.log_prefix} 获取提醒上下文失败: {e}") + return "无法加载相关的聊天记录。" + + async def _generate_proactive_content_and_send(self, action_result: Dict[str, Any], trigger_event: ProactiveTriggerEvent): """ 获取实时信息,构建最终的生成提示词,并生成和发送主动回复。 diff --git a/src/plugins/built_in/at_user_plugin/plugin.py b/src/plugins/built_in/at_user_plugin/plugin.py index bfb923963..cdaf7163e 100644 --- a/src/plugins/built_in/at_user_plugin/plugin.py +++ b/src/plugins/built_in/at_user_plugin/plugin.py @@ -69,49 +69,15 @@ class AtAction(BaseAction): if not member_list: return False, "群成员列表为空" - # 优化用户匹配逻辑 - best_match = None - user_id = None - - # 1. 完全精确匹配 - for member in member_list: - card = member.get("card", "") - nickname = member.get("nickname", "") - if user_name == card or user_name == nickname: - best_match = card if user_name == card else nickname - user_id = member["user_id"] - logger.info(f"找到完全精确匹配: '{user_name}' -> '{best_match}' (ID: {user_id})") - break + # 使用模糊匹配找到最接近的用户名 + choices = {member["card"] or member["nickname"]: member["user_id"] for member in member_list} + best_match, score = process.extractOne(user_name, choices.keys()) - # 2. 包含关系匹配 - if not best_match: - containing_matches = [] - for member in member_list: - card = member.get("card", "") - nickname = member.get("nickname", "") - if user_name in card: - containing_matches.append((card, member["user_id"])) - elif user_name in nickname: - containing_matches.append((nickname, member["user_id"])) - - if containing_matches: - # 选择最短的匹配项,因为通常更精确 - best_match, user_id = min(containing_matches, key=lambda x: len(x[0])) - logger.info(f"找到包含关系匹配: '{user_name}' -> '{best_match}' (ID: {user_id})") - - # 3. 模糊匹配作为兜底 - if not best_match: - choices = {member["card"] or member["nickname"]: member["user_id"] for member in member_list} - fuzzy_match, score = process.extractOne(user_name, choices.keys()) - if score >= 60: # 维持较高的阈值 - best_match = fuzzy_match - user_id = choices[best_match] - logger.info(f"找到模糊匹配: '{user_name}' -> '{best_match}' (ID: {user_id}, Score: {score})") - - if not best_match: - logger.warning(f"所有匹配策略都未能找到用户: '{user_name}'") + if score < 30: # 设置一个匹配度阈值 + logger.info(f"找不到与 '{user_name}' 高度匹配的用户 (最佳匹配: {best_match}, 分数: {score})") return False, "用户不存在" - + + user_id = choices[best_match] user_info = {"user_id": user_id, "user_nickname": best_match} try: @@ -126,23 +92,13 @@ class AtAction(BaseAction): return False, "聊天流不存在" replyer = DefaultReplyer(chat_stream) - # 优化提示词,消除记忆割裂感 - reminder_task = at_message.replace("定时提醒:", "").strip() - extra_info = f"""你之前记下了一个提醒任务:'{reminder_task}' -现在时间到了,你需要去提醒用户 '{user_name}'。 - -**重要规则**: -- 你的任务**只**是生成提醒的**内容**。 -- **绝对不要**在你的回复中包含任何`@`符号或者目标用户的名字。真正的@操作会由系统自动完成。 -- 像一个朋友一样,自然地完成这个提醒,而不是生硬地复述任务。 - -请直接输出提醒的**内容**。""" - + extra_info = f"你需要艾特用户 {user_name} 并回复他们说: {at_message}" + success, llm_response, _ = await replyer.generate_reply_with_context( - reply_to=f"是时候提醒'{user_name}'了", # 内部上下文,更符合执行任务的语境 + reply_to=f"{user_name}:{at_message}", extra_info=extra_info, enable_tool=False, - from_plugin=True # 标记为插件调用,以便LLM更好地理解上下文 + from_plugin=False ) if not success or not llm_response: From 3edda25ae1ae5be13376273a8ddfc192163bc6ec Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 13 Sep 2025 12:54:23 +0800 Subject: [PATCH 28/40] =?UTF-8?q?refactor(at=5Fuser):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=89=BE=E7=89=B9=E7=94=A8=E6=88=B7=E6=8F=92=E4=BB=B6=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E5=B9=B6=E7=A7=BB=E9=99=A4=E6=A8=A1=E7=B3=8A=E5=8C=B9?= =?UTF-8?q?=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构了艾特用户插件,主要改动如下: - 移除 `fuzzywuzzy` 依赖和相关的模糊匹配逻辑,改为直接通过 `person_info_manager` 精确查找用户。 - 优化了 `AtAction` 的执行流程,现在通过调用 `DefaultReplyer` 生成更智能、更符合上下文的回复内容,而不是发送固定文本。 - 新增了 `/at` 命令,允许用户通过指令直接艾特指定用户并发送消息。 - 删除了 `proactive_thinker.py` 中不再使用的 `_get_reminder_context` 方法,以清理与旧提醒功能相关的代码。 --- .../chat_loop/proactive/proactive_thinker.py | 14 -- src/plugins/built_in/at_user_plugin/plugin.py | 153 ++++++++++-------- 2 files changed, 88 insertions(+), 79 deletions(-) diff --git a/src/chat/chat_loop/proactive/proactive_thinker.py b/src/chat/chat_loop/proactive/proactive_thinker.py index d63820710..4abaccebd 100644 --- a/src/chat/chat_loop/proactive/proactive_thinker.py +++ b/src/chat/chat_loop/proactive/proactive_thinker.py @@ -138,20 +138,6 @@ class ProactiveThinker: except Exception as e: logger.error(f"{self.context.log_prefix} 主动思考执行异常: {e}") logger.error(traceback.format_exc()) - - async def _get_reminder_context(self, message_id: str) -> str: - """获取提醒消息的上下文""" - try: - # 只获取那一条消息 - message_record = await db_get(Messages, {"message_id": message_id}, single_result=True) - if message_record: - # 使用 build_readable_messages_with_id 来格式化单条消息 - chat_context_block, _ = build_readable_messages_with_id(messages=[message_record]) - return chat_context_block - return "无法加载相关的聊天记录。" - except Exception as e: - logger.error(f"{self.context.log_prefix} 获取提醒上下文失败: {e}") - return "无法加载相关的聊天记录。" async def _generate_proactive_content_and_send(self, action_result: Dict[str, Any], trigger_event: ProactiveTriggerEvent): diff --git a/src/plugins/built_in/at_user_plugin/plugin.py b/src/plugins/built_in/at_user_plugin/plugin.py index cdaf7163e..7a80b8ab6 100644 --- a/src/plugins/built_in/at_user_plugin/plugin.py +++ b/src/plugins/built_in/at_user_plugin/plugin.py @@ -8,6 +8,7 @@ from src.plugin_system import ( ActionInfo, ActionActivationType, ) +from src.person_info.person_info import get_person_info_manager from src.common.logger import get_logger from src.plugin_system.base.component_types import ChatType @@ -20,16 +21,16 @@ class AtAction(BaseAction): # === 基本信息(必须填写)=== action_name = "at_user" action_description = "发送艾特消息" - activation_type = ActionActivationType.LLM_JUDGE + activation_type = ActionActivationType.LLM_JUDGE # 消息接收时激活(?) parallel_action = False chat_type_allow = ChatType.GROUP # === 功能描述(必须填写)=== - action_parameters = {"user_name": "需要艾特用户的名字", "at_message": "艾特用户时要发送的消息"} + action_parameters = {"user_name": "需要艾特用户的名字", "at_message": "艾特用户时要发送的消,注意消息里不要有@"} action_require = [ - "当用户明确要求你去'叫'、'喊'、'提醒'或'艾特'某人时使用", - "当你判断,为了让特定的人看到消息,需要代表用户去呼叫他/她时使用", - "例如:'你去叫一下张三','提醒一下李四开会'", + "当需要艾特某个用户时使用", + "当你需要提醒特定用户查看消息时使用", + "在回复中需要明确指向某个用户时使用", ] llm_judge_prompt = """ 判定是否需要使用艾特用户动作的条件: @@ -47,43 +48,24 @@ class AtAction(BaseAction): if not user_name or not at_message: logger.warning("艾特用户的动作缺少必要参数。") + await self.store_action_info( + action_build_into_prompt=True, + action_prompt_display=f"执行了艾特用户动作:艾特用户 {user_name} 并发送消息: {at_message},失败了,因为没有提供必要参数", + action_done=False, + ) return False, "缺少必要参数" - from src.plugin_system.apis import send_api - from fuzzywuzzy import process - - group_id = self.chat_stream.group_info.group_id - if not group_id: - return False, "无法获取群组ID" - - response = await send_api.adapter_command_to_stream( - action="get_group_member_list", - params={"group_id": group_id}, - stream_id=self.chat_id, - ) - - if response.get("status") != "ok": - return False, f"获取群成员列表失败: {response.get('message')}" - - member_list = response.get("data", []) - if not member_list: - return False, "群成员列表为空" - - # 使用模糊匹配找到最接近的用户名 - choices = {member["card"] or member["nickname"]: member["user_id"] for member in member_list} - best_match, score = process.extractOne(user_name, choices.keys()) - - if score < 30: # 设置一个匹配度阈值 - logger.info(f"找不到与 '{user_name}' 高度匹配的用户 (最佳匹配: {best_match}, 分数: {score})") + user_info = await get_person_info_manager().get_person_info_by_name(user_name) + if not user_info or not user_info.get("user_id"): + logger.info(f"找不到名为 '{user_name}' 的用户。") return False, "用户不存在" - - user_id = choices[best_match] - user_info = {"user_id": user_id, "user_nickname": best_match} try: + # 使用回复器生成艾特回复,而不是直接发送命令 from src.chat.replyer.default_generator import DefaultReplyer from src.chat.message_receive.chat_stream import get_chat_manager - + + # 获取当前聊天流 chat_manager = get_chat_manager() chat_stream = chat_manager.get_stream(self.chat_id) @@ -91,56 +73,97 @@ class AtAction(BaseAction): logger.error(f"找不到聊天流: {self.stream_id}") return False, "聊天流不存在" + # 创建回复器实例 replyer = DefaultReplyer(chat_stream) + + # 构建回复对象,将艾特消息作为回复目标 + reply_to = f"{user_name}:{at_message}" extra_info = f"你需要艾特用户 {user_name} 并回复他们说: {at_message}" - success, llm_response, _ = await replyer.generate_reply_with_context( - reply_to=f"{user_name}:{at_message}", + # 使用回复器生成回复 + success, llm_response, prompt = await replyer.generate_reply_with_context( + reply_to=reply_to, extra_info=extra_info, - enable_tool=False, + enable_tool=False, # 艾特回复通常不需要工具调用 from_plugin=False ) - if not success or not llm_response: + if success and llm_response: + # 获取生成的回复内容 + reply_content = llm_response.get("content", "") + if reply_content: + # 获取用户QQ号,发送真正的艾特消息 + user_id = user_info.get("user_id") + + # 发送真正的艾特命令,使用回复器生成的智能内容 + await self.send_command( + "SEND_AT_MESSAGE", + args={"qq_id": user_id, "text": reply_content}, + display_message=f"艾特用户 {user_name} 并发送智能回复: {reply_content}", + ) + + await self.store_action_info( + action_build_into_prompt=True, + action_prompt_display=f"执行了艾特用户动作:艾特用户 {user_name} 并发送智能回复: {reply_content}", + action_done=True, + ) + + logger.info(f"成功通过回复器生成智能内容并发送真正的艾特消息给 {user_name}: {reply_content}") + return True, "智能艾特消息发送成功" + else: + logger.warning("回复器生成了空内容") + return False, "回复内容为空" + else: logger.error("回复器生成回复失败") return False, "回复生成失败" - - final_message_raw = llm_response.get("content", "") - if not final_message_raw: - logger.warning("回复器生成了空内容") - return False, "回复内容为空" - - # 对LLM生成的内容进行后处理,解析[SPLIT]标记并将分段消息合并 - from src.chat.utils.utils import process_llm_response - final_message_segments = process_llm_response(final_message_raw, enable_splitter=True, enable_chinese_typo=False) - final_message = " ".join(final_message_segments) - - await self.send_command( - "SEND_AT_MESSAGE", - args={"group_id": self.chat_stream.group_info.group_id, "qq_id": user_id, "text": final_message}, - display_message=f"艾特用户 {user_name} 并发送消息: {final_message}", - ) - - await self.store_action_info( - action_build_into_prompt=True, - action_prompt_display=f"执行了艾特用户动作:艾特用户 {user_name} 并发送消息: {final_message}", - action_done=True, - ) - - logger.info(f"成功发送艾特消息给 {user_name}: {final_message}") - return True, "艾特消息发送成功" except Exception as e: logger.error(f"执行艾特用户动作时发生异常: {e}", exc_info=True) + await self.store_action_info( + action_build_into_prompt=True, + action_prompt_display=f"执行艾特用户动作失败:{str(e)}", + action_done=False, + ) return False, f"执行失败: {str(e)}" +class AtCommand(BaseCommand): + command_name: str = "at_user" + description: str = "通过名字艾特用户" + command_pattern: str = r"/at\s+@?(?P[\S]+)(?:\s+(?P.*))?" + + async def execute(self) -> Tuple[bool, str, bool]: + name = self.matched_groups.get("name") + text = self.matched_groups.get("text", "") + + if not name: + await self.send_text("请指定要艾特的用户名称。") + return False, "缺少用户名称", True + + person_info_manager = get_person_info_manager() + user_info = await person_info_manager.get_person_info_by_name(name) + + if not user_info or not user_info.get("user_id"): + await self.send_text(f"找不到名为 '{name}' 的用户。") + return False, "用户不存在", True + + user_id = user_info.get("user_id") + + await self.send_command( + "SEND_AT_MESSAGE", + args={"qq_id": user_id, "text": text}, + display_message=f"艾特用户 {name} 并发送消息: {text}", + ) + + return True, "艾特消息已发送", True + + @register_plugin class AtUserPlugin(BasePlugin): plugin_name: str = "at_user_plugin" enable_plugin: bool = True dependencies: list[str] = [] - python_dependencies: list[str] = ["fuzzywuzzy", "python-Levenshtein"] + python_dependencies: list[str] = [] config_file_name: str = "config.toml" config_schema: dict = {} From befdb796b9f94117731833716e2d71fd8595d963 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 13 Sep 2025 12:59:44 +0800 Subject: [PATCH 29/40] =?UTF-8?q?refactor(at=5Fuser):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E6=8F=8F=E8=BF=B0=E5=B9=B6=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E6=B5=81=E8=8E=B7=E5=8F=96=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新了 `action_parameters` 和 `action_require` 的描述,使其更加清晰和准确,提升了LLM在不同场景下判断的准确性。 - 修复了 `chat_stream` 的获取逻辑,优先使用 `self.chat_stream`,确保在不同调用路径下都能正确获取到聊天流实例。 --- src/plugins/built_in/at_user_plugin/plugin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugins/built_in/at_user_plugin/plugin.py b/src/plugins/built_in/at_user_plugin/plugin.py index 7a80b8ab6..ba40903cd 100644 --- a/src/plugins/built_in/at_user_plugin/plugin.py +++ b/src/plugins/built_in/at_user_plugin/plugin.py @@ -26,11 +26,11 @@ class AtAction(BaseAction): chat_type_allow = ChatType.GROUP # === 功能描述(必须填写)=== - action_parameters = {"user_name": "需要艾特用户的名字", "at_message": "艾特用户时要发送的消,注意消息里不要有@"} + action_parameters = {"user_name": "需要艾特用户的名字", "at_message": "艾特用户时要发送的消息"} action_require = [ - "当需要艾特某个用户时使用", - "当你需要提醒特定用户查看消息时使用", - "在回复中需要明确指向某个用户时使用", + "当用户明确要求你去'叫'、'喊'、'提醒'或'艾特'某人时使用", + "当你判断,为了让特定的人看到消息,需要代表用户去呼叫他/她时使用", + "例如:'你去叫一下张三','提醒一下李四开会'", ] llm_judge_prompt = """ 判定是否需要使用艾特用户动作的条件: @@ -67,10 +67,10 @@ class AtAction(BaseAction): # 获取当前聊天流 chat_manager = get_chat_manager() - chat_stream = chat_manager.get_stream(self.chat_id) + chat_stream = self.chat_stream or chat_manager.get_stream(self.chat_id) if not chat_stream: - logger.error(f"找不到聊天流: {self.stream_id}") + logger.error(f"找不到聊天流: {self.chat_stream}") return False, "聊天流不存在" # 创建回复器实例 From 91d0ebf45d4f717d1f7ba6fef5f3b834d75c038c Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 13 Sep 2025 13:23:21 +0800 Subject: [PATCH 30/40] =?UTF-8?q?feat:=E4=B8=80=E4=B8=AA=E8=83=BD=E4=BB=8E?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E4=B8=AD=E6=99=BA=E8=83=BD=E8=AF=86=E5=88=AB?= =?UTF-8?q?=E5=B9=B6=E8=AE=BE=E7=BD=AE=E5=AE=9A=E6=97=B6=E6=8F=90=E9=86=92?= =?UTF-8?q?=E7=9A=84=E5=8A=A8=E4=BD=9C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/reminder_plugin/_manifest.json | 9 + src/plugins/reminder_plugin/plugin.py | 198 +++++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 src/plugins/reminder_plugin/_manifest.json create mode 100644 src/plugins/reminder_plugin/plugin.py diff --git a/src/plugins/reminder_plugin/_manifest.json b/src/plugins/reminder_plugin/_manifest.json new file mode 100644 index 000000000..58c9fc9e4 --- /dev/null +++ b/src/plugins/reminder_plugin/_manifest.json @@ -0,0 +1,9 @@ +{ + "manifest_version": 1, + "name": "智能提醒插件", + "version": "1.0.0", + "description": "一个能从对话中智能识别并设置定时提醒的插件。", + "author": { + "name": "墨墨" + } +} \ No newline at end of file diff --git a/src/plugins/reminder_plugin/plugin.py b/src/plugins/reminder_plugin/plugin.py new file mode 100644 index 000000000..8a833f5be --- /dev/null +++ b/src/plugins/reminder_plugin/plugin.py @@ -0,0 +1,198 @@ +import asyncio +from datetime import datetime +from typing import List, Tuple, Type +from dateutil.parser import parse as parse_datetime + +from src.common.logger import get_logger +from src.manager.async_task_manager import AsyncTask, async_task_manager +from src.person_info.person_info import get_person_info_manager +from src.plugin_system import ( + BaseAction, + ActionInfo, + BasePlugin, + register_plugin, + ActionActivationType, +) +from src.plugin_system.apis import send_api +from src.plugin_system.base.component_types import ChatType + +logger = get_logger(__name__) + + +# ============================ AsyncTask ============================ + +class ReminderTask(AsyncTask): + def __init__(self, delay: float, stream_id: str, is_group: bool, target_user_id: str, target_user_name: str, event_details: str, creator_name: str): + super().__init__(task_name=f"ReminderTask_{target_user_id}_{datetime.now().timestamp()}") + self.delay = delay + self.stream_id = stream_id + self.is_group = is_group + self.target_user_id = target_user_id + self.target_user_name = target_user_name + self.event_details = event_details + self.creator_name = creator_name + + async def run(self): + try: + if self.delay > 0: + logger.info(f"等待 {self.delay:.2f} 秒后执行提醒...") + await asyncio.sleep(self.delay) + + logger.info(f"执行提醒任务: 给 {self.target_user_name} 发送关于 '{self.event_details}' 的提醒") + + reminder_text = f"叮咚!这是 {self.creator_name} 让我准时提醒你的事情:\n\n{self.event_details}" + + if self.is_group: + # 在群聊中,构造 @ 消息段并发送 + group_id = self.stream_id.split('_')[-1] if '_' in self.stream_id else self.stream_id + message_payload = [ + {"type": "at", "data": {"qq": self.target_user_id}}, + {"type": "text", "data": {"text": f" {reminder_text}"}} + ] + await send_api.adapter_command_to_stream( + action="send_group_msg", + params={"group_id": group_id, "message": message_payload}, + stream_id=self.stream_id + ) + else: + # 在私聊中,直接发送文本 + await send_api.text_to_stream(text=reminder_text, stream_id=self.stream_id) + + logger.info(f"提醒任务 {self.task_name} 成功完成。") + + except Exception as e: + logger.error(f"执行提醒任务 {self.task_name} 时出错: {e}", exc_info=True) + + +# =============================== Actions =============================== + +class RemindAction(BaseAction): + """一个能从对话中智能识别并设置定时提醒的动作。""" + + # === 基本信息 === + action_name = "set_reminder" + action_description = "根据用户的对话内容,智能地设置一个未来的提醒事项。" + activation_type = ActionActivationType.LLM_JUDGE + chat_type_allow = ChatType.ALL + + # === LLM 判断与参数提取 === + llm_judge_prompt = """ + 判断用户是否意图设置一个未来的提醒。 + - 必须包含明确的时间点或时间段(如“十分钟后”、“明天下午3点”、“周五”)。 + - 必须包含一个需要被提醒的事件。 + - 可能会包含需要提醒的特定人物。 + - 如果只是普通的聊天或询问时间,则不应触发。 + + 示例: + - "半小时后提醒我开会" -> 是 + - "明天下午三点叫张三来一下" -> 是 + - "别忘了周五把报告交了" -> 是 + - "现在几点了?" -> 否 + - "我明天下午有空" -> 否 + + 请只回答"是"或"否"。 + """ + action_parameters = { + "user_name": "需要被提醒的人的称呼或名字,如果没有明确指定给某人,则默认为'自己'", + "remind_time": "描述提醒时间的自然语言字符串,例如'十分钟后'或'明天下午3点'", + "event_details": "需要提醒的具体事件内容" + } + action_require = [ + "当用户请求在未来的某个时间点提醒他/她或别人某件事时使用", + "适用于包含明确时间信息和事件描述的对话", + "例如:'10分钟后提醒我收快递'、'明天早上九点喊一下李四参加晨会'" + ] + + async def execute(self) -> Tuple[bool, str]: + """执行设置提醒的动作""" + user_name = self.action_data.get("user_name") + remind_time_str = self.action_data.get("remind_time") + event_details = self.action_data.get("event_details") + + if not all([user_name, remind_time_str, event_details]): + missing_params = [p for p, v in {"user_name": user_name, "remind_time": remind_time_str, "event_details": event_details}.items() if not v] + error_msg = f"缺少必要的提醒参数: {', '.join(missing_params)}" + logger.warning(f"[ReminderPlugin] LLM未能提取完整参数: {error_msg}") + return False, error_msg + + # 1. 解析时间 + try: + assert isinstance(remind_time_str, str) + target_time = parse_datetime(remind_time_str, fuzzy=True) + except Exception as e: + logger.error(f"[ReminderPlugin] 无法解析时间字符串 '{remind_time_str}': {e}") + await self.send_text(f"抱歉,我无法理解您说的时间 '{remind_time_str}',提醒设置失败。") + return False, f"无法解析时间 '{remind_time_str}'" + + now = datetime.now() + if target_time <= now: + await self.send_text("提醒时间必须是一个未来的时间点哦,提醒设置失败。") + return False, "提醒时间必须在未来" + + delay_seconds = (target_time - now).total_seconds() + + # 2. 解析用户 + person_manager = get_person_info_manager() + user_id_to_remind = None + user_name_to_remind = "" + + assert isinstance(user_name, str) + + if user_name.strip() in ["自己", "我", "me"]: + user_id_to_remind = self.user_id + user_name_to_remind = self.user_nickname + else: + user_info = await person_manager.get_person_info_by_name(user_name) + if not user_info or not user_info.get("user_id"): + logger.warning(f"[ReminderPlugin] 找不到名为 '{user_name}' 的用户") + await self.send_text(f"抱歉,我的联系人里找不到叫做 '{user_name}' 的人,提醒设置失败。") + return False, f"用户 '{user_name}' 不存在" + user_id_to_remind = user_info.get("user_id") + user_name_to_remind = user_name + + # 3. 创建并调度异步任务 + try: + assert user_id_to_remind is not None + assert event_details is not None + + reminder_task = ReminderTask( + delay=delay_seconds, + stream_id=self.chat_id, + is_group=self.is_group, + target_user_id=str(user_id_to_remind), + target_user_name=str(user_name_to_remind), + event_details=str(event_details), + creator_name=str(self.user_nickname) + ) + await async_task_manager.add_task(reminder_task) + + # 4. 发送确认消息 + confirm_message = f"好的,我记下了。\n将在 {target_time.strftime('%Y-%m-%d %H:%M:%S')} 提醒 {user_name_to_remind}:\n{event_details}" + await self.send_text(confirm_message) + + return True, "提醒设置成功" + except Exception as e: + logger.error(f"[ReminderPlugin] 创建提醒任务时出错: {e}", exc_info=True) + await self.send_text("抱歉,设置提醒时发生了一点内部错误。") + return False, "设置提醒时发生内部错误" + + +# =============================== Plugin =============================== + +@register_plugin +class ReminderPlugin(BasePlugin): + """一个能从对话中智能识别并设置定时提醒的插件。""" + + # --- 插件基础信息 --- + plugin_name = "reminder_plugin" + enable_plugin = True + dependencies = [] + python_dependencies = [] + config_file_name = "config.toml" + config_schema = {} + + def get_plugin_components(self) -> List[Tuple[ActionInfo, Type[BaseAction]]]: + """注册插件的所有功能组件。""" + return [ + (RemindAction.get_action_info(), RemindAction) + ] From 2db6be9b5b91a2f39e8de138b1d672f8a896bd4f Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 13 Sep 2025 14:46:50 +0800 Subject: [PATCH 31/40] =?UTF-8?q?fix(chat):=20=E4=BF=AE=E5=A4=8D=E8=AE=A1?= =?UTF-8?q?=E5=88=92=E8=BF=87=E6=BB=A4=E5=99=A8=E5=92=8C=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E5=99=A8=E4=B8=AD=E7=9A=84=E6=BD=9C=E5=9C=A8?= =?UTF-8?q?=E7=A9=BA=E6=8C=87=E9=92=88=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 `plan_filter.py` 中,当LLM未指定 `target_message_id` 时,明确将目标消息设置为最新的消息,避免后续操作因 `target_message_dict` 未定义而出错。 - 在 `chat_message_builder.py` 中,为 `replace_user_references_sync` 函数增加了对 `content` 为空的检查,防止后续处理引发异常。 - 将数据库查询从 `.scalar()` 改为 `.scalar_one_or_none()`,以更清晰地处理未找到结果的情况。 --- src/chat/planner_actions/plan_filter.py | 12 +++++++----- src/chat/utils/chat_message_builder.py | 7 +++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/chat/planner_actions/plan_filter.py b/src/chat/planner_actions/plan_filter.py index 00097870c..32ad9586b 100644 --- a/src/chat/planner_actions/plan_filter.py +++ b/src/chat/planner_actions/plan_filter.py @@ -233,11 +233,13 @@ class PlanFilter: if action not in ["no_action", "no_reply", "do_nothing", "proactive_reply"]: if target_message_id := action_json.get("target_message_id"): target_message_dict = self._find_message_by_id(target_message_id, message_id_list) - if target_message_dict is None: - target_message_dict = self._get_latest_message(message_id_list) - if target_message_dict: - from src.common.data_models.database_data_model import DatabaseMessages - target_message_obj = DatabaseMessages(**target_message_dict) + else: + # 如果LLM没有指定target_message_id,我们就默认选择最新的一条消息 + target_message_dict = self._get_latest_message(message_id_list) + + if target_message_dict: + from src.common.data_models.database_data_model import DatabaseMessages + target_message_obj = DatabaseMessages(**target_message_dict) available_action_names = list(plan.available_actions.keys()) if action not in ["no_action", "no_reply", "reply", "do_nothing", "proactive_reply"] and action not in available_action_names: diff --git a/src/chat/utils/chat_message_builder.py b/src/chat/utils/chat_message_builder.py index e6843874f..83b1b0587 100644 --- a/src/chat/utils/chat_message_builder.py +++ b/src/chat/utils/chat_message_builder.py @@ -35,6 +35,9 @@ def replace_user_references_sync( Returns: str: 处理后的内容字符串 """ + if not content: + return "" + if name_resolver is None: person_info_manager = get_person_info_manager() @@ -817,8 +820,8 @@ def build_pic_mapping_info(pic_id_mapping: Dict[str, str]) -> str: description = "[图片内容未知]" # 默认描述 try: with get_db_session() as session: - image = session.execute(select(Images).where(Images.image_id == pic_id)).scalar() - if image and image.description: + image = session.execute(select(Images).where(Images.image_id == pic_id)).scalar_one_or_none() + if image and image.description: # type: ignore description = image.description except Exception: # 如果查询失败,保持默认描述 From 7ea769996ce6479c31519a8ff8db96943a241596 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 13 Sep 2025 15:14:18 +0800 Subject: [PATCH 32/40] =?UTF-8?q?refactor(chat):=20=E4=BC=98=E5=8C=96plan?= =?UTF-8?q?=5Ffilter=E4=B8=AD=E7=9A=84=E5=9B=9E=E5=A4=8D=E5=8A=A8=E4=BD=9C?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入 `reply_action_added` 标志位,确保在一次处理中只添加一个回复类型的动作(如 `reply` 或 `proactive_reply`)。 - 优化了循环和条件判断逻辑,使代码更清晰、更健壮,防止因LLM返回多个回复动作而导致的意外行为。 --- src/chat/planner_actions/plan_filter.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/chat/planner_actions/plan_filter.py b/src/chat/planner_actions/plan_filter.py index 32ad9586b..f56bcf60e 100644 --- a/src/chat/planner_actions/plan_filter.py +++ b/src/chat/planner_actions/plan_filter.py @@ -49,6 +49,7 @@ class PlanFilter: llm_content, _ = await self.planner_llm.generate_response_async(prompt=prompt) if llm_content: + logger.debug(f"LLM a原始返回: {llm_content}") parsed_json = orjson.loads(repair_json(llm_content)) if isinstance(parsed_json, dict): @@ -56,13 +57,33 @@ class PlanFilter: if isinstance(parsed_json, list): final_actions = [] + reply_action_added = False + # 定义回复类动作的集合,方便扩展 + reply_action_types = {"reply", "proactive_reply"} + for item in parsed_json: - if isinstance(item, dict): + if not isinstance(item, dict): + continue + + # 预解析 action_type 来进行判断 + action_type = item.get("action", "no_action") + + if action_type in reply_action_types: + if not reply_action_added: + final_actions.extend( + await self._parse_single_action( + item, used_message_id_list, plan + ) + ) + reply_action_added = True + else: + # 非回复类动作直接添加 final_actions.extend( await self._parse_single_action( item, used_message_id_list, plan ) ) + plan.decided_actions = self._filter_no_actions(final_actions) except Exception as e: From 5c1a7ab33bb95653d7b5962d861e49cc4f92eb8c Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 13 Sep 2025 15:21:00 +0800 Subject: [PATCH 33/40] feat(db): add focus_energy to ChatStreams model Adds a new `focus_energy` column to the `ChatStreams` table. This field will be used to track the user's focus level, defaulting to 1.0. --- src/common/database/sqlalchemy_models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/database/sqlalchemy_models.py b/src/common/database/sqlalchemy_models.py index a5b431a1c..464b38e9f 100644 --- a/src/common/database/sqlalchemy_models.py +++ b/src/common/database/sqlalchemy_models.py @@ -53,6 +53,7 @@ class ChatStreams(Base): user_cardname = Column(Text, nullable=True) energy_value = Column(Float, nullable=True, default=5.0) sleep_pressure = Column(Float, nullable=True, default=0.0) + focus_energy = Column(Float, nullable=True, default=1.0) __table_args__ = ( Index("idx_chatstreams_stream_id", "stream_id"), From b9fe4b793fcd45b505f6c15d4ec76dbdea575958 Mon Sep 17 00:00:00 2001 From: Windpicker-owo Date: Sat, 13 Sep 2025 16:25:13 +0800 Subject: [PATCH 34/40] =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/planner_actions/plan_generator.py | 1 - src/chat/replyer/default_generator.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/chat/planner_actions/plan_generator.py b/src/chat/planner_actions/plan_generator.py index f3537b19b..5dd1b680c 100644 --- a/src/chat/planner_actions/plan_generator.py +++ b/src/chat/planner_actions/plan_generator.py @@ -58,7 +58,6 @@ class PlanGenerator: target_info = TargetPersonInfo(**chat_target_info_dict) available_actions = self._get_available_actions() - chat_history_raw = get_raw_msg_before_timestamp_with_chat( chat_id=self.chat_id, timestamp=time.time(), diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 8f8323d6f..a28fa4120 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -262,7 +262,7 @@ class DefaultReplyer: available_actions = {} llm_response = None try: - # 3. 构建 Prompt + # 构建 Prompt with Timer("构建Prompt", {}): # 内部计时器,可选保留 prompt = await self.build_prompt_reply_context( reply_to=reply_to, From dbc6bb7562abef6926e87a36320d7c8aa288035c Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 13 Sep 2025 20:30:13 +0800 Subject: [PATCH 35/40] =?UTF-8?q?fix(qzone):=20=E4=BF=AE=E5=A4=8D=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E6=9C=AC=E5=9C=B0=E5=9B=BE=E7=89=87=E6=97=B6=E6=9C=AA?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=9B=BE=E7=89=87=E7=9B=AE=E5=BD=95=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E7=9A=84=E6=BD=9C=E5=9C=A8=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 `_load_local_images` 方法中,如果 `image_dir` 为 `None` 或空字符串,`os.path.exists` 会抛出 `TypeError`。本次修改增加了对 `image_dir` 变量本身的检查,确保在路径有效时才进行存在性判断,从而避免了因未配置图片目录而导致的程序崩溃。 --- .../built_in/maizone_refactored/services/qzone_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/built_in/maizone_refactored/services/qzone_service.py b/src/plugins/built_in/maizone_refactored/services/qzone_service.py index 2c7dddaec..abb5f97e6 100644 --- a/src/plugins/built_in/maizone_refactored/services/qzone_service.py +++ b/src/plugins/built_in/maizone_refactored/services/qzone_service.py @@ -346,8 +346,8 @@ class QZoneService: def _load_local_images(self, image_dir: str) -> List[bytes]: """随机加载本地图片(不删除文件)""" images = [] - if not os.path.exists(image_dir): - logger.warning(f"图片目录不存在: {image_dir}") + if not image_dir or not os.path.exists(image_dir): + logger.warning(f"图片目录不存在或未配置: {image_dir}") return images try: From 862f0df15ff637350234012ed7189bd0ebb06c6b Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 13 Sep 2025 21:10:49 +0800 Subject: [PATCH 36/40] =?UTF-8?q?fix(chat):=20=E4=BF=AE=E5=A4=8D=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E7=94=A8=E6=88=B7ID=E6=97=B6=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E4=BA=86=E9=94=99=E8=AF=AF=E7=9A=84=E9=94=AE=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 `action_message.get("user_id", "")` 修改为 `action_message.get("chat_info_user_id", "")`,以确保从 `action_message` 字典中正确获取用户ID。 chore: 更新 .gitignore 文件 添加 `src/chat/planner_actions/新建 文本文档.txt` 到忽略列表,以避免将临时文件提交到版本库。 --- .gitignore | 1 + src/chat/chat_loop/cycle_processor.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f03fa1b26..b63572ab4 100644 --- a/.gitignore +++ b/.gitignore @@ -341,3 +341,4 @@ rust_video/Cargo.lock .claude/settings.local.json package-lock.json package.json +src/chat/planner_actions/新建 文本文档.txt diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index 2e290ae9d..06f1b6a36 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -84,7 +84,7 @@ class CycleProcessor: # 获取用户信息并生成回复提示 person_id = person_info_manager.get_person_id( platform, - action_message.get("user_id", ""), + action_message.get("chat_info_user_id", ""), ) person_name = await person_info_manager.get_value(person_id, "person_name") action_prompt_display = f"你对{person_name}进行了回复:{reply_text}" From f6bd0d40581c4428fdfbf9abb7fd8afe680ffde8 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 13 Sep 2025 21:37:47 +0800 Subject: [PATCH 37/40] =?UTF-8?q?feat(chat):=20=E5=A2=9E=E5=8A=A0=E5=9B=9E?= =?UTF-8?q?=E5=A4=8D=E5=8A=A8=E4=BD=9C=E7=9A=84=E6=B6=88=E6=81=AF=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在执行回复动作前,增加对 `action_message` 的日志输出,方便调试和追踪回复流程中的具体消息内容。 --- src/chat/chat_loop/cycle_processor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index 06f1b6a36..c26b60876 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -299,6 +299,7 @@ class CycleProcessor: logger.info(f"{self.log_prefix} 正在执行文本回复...") for action in reply_actions: target_user_id = action.get("action_message",{}).get("chat_info_user_id","") + logger.info(action.get("action_message",{})) if target_user_id == global_config.bot.qq_account and not global_config.chat.allow_reply_self: logger.warning("选取的reply的目标为bot自己,跳过reply action") continue From a27639ea689ba411964a59405817669be0cd56ef Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Sat, 13 Sep 2025 21:40:46 +0800 Subject: [PATCH 38/40] =?UTF-8?q?refactor(chat):=20=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E8=AE=B0=E5=BF=86=E6=BF=80=E6=B4=BB=E6=96=B9=E6=B3=95=E7=9A=84?= =?UTF-8?q?=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更新 `activate_memory_with_chat_history` 的调用方式,使用 `target_message` 对象来替代独立的 `target_user` 和 `chat_id` 参数。 此举将相关上下文封装到单个对象中,使得接口更加清晰,提高了代码的可维护性。 --- src/chat/utils/prompt.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/chat/utils/prompt.py b/src/chat/utils/prompt.py index 76c476407..dd4fee364 100644 --- a/src/chat/utils/prompt.py +++ b/src/chat/utils/prompt.py @@ -583,9 +583,8 @@ class Prompt: # 激活长期记忆 memory_activator = MemoryActivator() running_memories = await memory_activator.activate_memory_with_chat_history( - chat_history=chat_history, - target_user=self.parameters.sender, - chat_id=self.parameters.chat_id + target_message=self.parameters.target, + chat_history_prompt=chat_history ) # 获取即时记忆 From 5b57d58db7eb758ec0fe4f1e0ee8bf1f6fe04839 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 13 Sep 2025 21:43:08 +0800 Subject: [PATCH 39/40] =?UTF-8?q?feat(chat):=20=E5=A2=9E=E5=8A=A0=E8=B0=83?= =?UTF-8?q?=E8=AF=95=E6=97=A5=E5=BF=97=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为了方便调试,在执行文本回复动作时,增加了一条详细的调试日志。这条日志会输出完整的 `action_message` 内容,并带有一个特殊的提示,方便在出现问题时快速定位和反馈。 --- src/chat/chat_loop/cycle_processor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/chat/chat_loop/cycle_processor.py b/src/chat/chat_loop/cycle_processor.py index c26b60876..0af7b1b85 100644 --- a/src/chat/chat_loop/cycle_processor.py +++ b/src/chat/chat_loop/cycle_processor.py @@ -299,7 +299,8 @@ class CycleProcessor: logger.info(f"{self.log_prefix} 正在执行文本回复...") for action in reply_actions: target_user_id = action.get("action_message",{}).get("chat_info_user_id","") - logger.info(action.get("action_message",{})) + action_message_test =action.get("action_message",{}) + logger.info(f"如果你探到这条日志请把它复制下来发到Q群里,如果你探到这条日志请把它复制下来发到Q群里,如果你探到这条日志请把它复制下来发到Q群里,调试内容:{action_message_test}") if target_user_id == global_config.bot.qq_account and not global_config.chat.allow_reply_self: logger.warning("选取的reply的目标为bot自己,跳过reply action") continue From 33a3b80edf3d20b185114b1d5fbf69e455caf16e Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Sat, 13 Sep 2025 22:44:46 +0800 Subject: [PATCH 40/40] =?UTF-8?q?fix(napcat):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=B9=B3=E5=8F=B0=E5=90=8D=E7=A7=B0=E4=B8=BA?= =?UTF-8?q?=20qq?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 Napcat 适配器中 maibot_server 的默认 platform_name 从 "napcat" 更改为 "qq",以解决消息路由中潜在的平台识别问题,确保兼容性。 --- src/plugins/built_in/napcat_adapter_plugin/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/built_in/napcat_adapter_plugin/plugin.py b/src/plugins/built_in/napcat_adapter_plugin/plugin.py index 5059a7fb6..952fcaccc 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/plugin.py +++ b/src/plugins/built_in/napcat_adapter_plugin/plugin.py @@ -318,7 +318,7 @@ class NapcatAdapterPlugin(BasePlugin): "maibot_server": { "host": ConfigField(type=str, default="localhost", description="麦麦在.env文件中设置的主机地址,即HOST字段"), "port": ConfigField(type=int, default=8000, description="麦麦在.env文件中设置的端口,即PORT字段"), - "platform_name": ConfigField(type=str, default="napcat", description="平台名称,用于消息路由"), + "platform_name": ConfigField(type=str, default="qq", description="平台名称,用于消息路由"), }, "voice": { "use_tts": ConfigField(type=bool, default=False, description="是否使用tts语音(请确保你配置了tts并有对应的adapter)"),