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/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index d601e7030..da1b94f90 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -136,7 +136,7 @@ def init_prompt(): 4. 不要浮夸,不要夸张修辞,不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。 最终请输出一条简短、完整且口语化的回复。 --------------------------------- + -------------------------------- {time_block} 注意不要复读你前面发过的内容,意思相近也不行。 @@ -1028,6 +1028,23 @@ class DefaultReplyer: prompt = Prompt(template=template_prompt.template, parameters=prompt_parameters) prompt_text = await prompt.build() + # --- 动态添加分割指令 --- + if global_config.response_splitter.enable and global_config.response_splitter.split_mode == "llm": + split_instruction = """ +## 消息分段艺术 +为了模仿真实人类的聊天节奏,你可以在需要时将一条回复分成几段发送。 + +**核心原则**: 只有当分段能**增强表达效果**或**控制信息节奏**时,才在断句处使用 `[SPLIT]` 标记。 + +**参考场景**: +- 当你想表达一个转折或停顿时。 +- 当你想先说结论,再补充说明时。 + +**任务**: 请结合你的智慧和人设,自然地决定是否需要分段。如果需要,请在最恰当的位置插入 `[SPLIT]` 标记。 +""" + # 将分段指令添加到提示词顶部 + prompt_text = f"{split_instruction}\n{prompt_text}" + return prompt_text async def build_prompt_rewrite_context( diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index 501bf382d..675bf4b85 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -331,8 +331,23 @@ def process_llm_response(text: str, enable_splitter: bool = True, enable_chinese ) if global_config.response_splitter.enable and enable_splitter: - split_sentences = split_into_sentences_w_remove_punctuation(cleaned_text) + logger.info(f"回复分割器已启用,模式: {global_config.response_splitter.split_mode}。") + + split_mode = global_config.response_splitter.split_mode + + if split_mode == "llm" and "[SPLIT]" in cleaned_text: + logger.debug("检测到 [SPLIT] 标记,使用 LLM 自定义分割。") + split_sentences_raw = cleaned_text.split("[SPLIT]") + split_sentences = [s.strip() for s in split_sentences_raw if s.strip()] + else: + if split_mode == "llm": + logger.debug("未检测到 [SPLIT] 标记,本次不进行分割。") + split_sentences = [cleaned_text] + else: # mode == "punctuation" + logger.debug("使用基于标点的传统模式进行分割。") + split_sentences = split_into_sentences_w_remove_punctuation(cleaned_text) else: + logger.debug("回复分割器已禁用。") split_sentences = [cleaned_text] sentences = [] 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 b97b5443f..6a557a342 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -385,6 +385,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="启用情感分析") + max_context_emojis: int = Field(default=30, description="每次随机传递给LLM的表情包最大数量,0为全部") class MemoryConfig(ValidatedConfigBase): @@ -475,6 +476,7 @@ class ResponseSplitterConfig(ValidatedConfigBase): """回复分割器配置类""" enable: bool = Field(default=True, description="启用") + split_mode: str = Field(default="llm", description="分割模式: 'llm' 或 'punctuation'") max_length: int = Field(default=256, description="最大长度") max_sentence_num: int = Field(default=3, description="最大句子数") enable_kaomoji_protection: bool = Field(default=False, description="启用颜文字保护") @@ -492,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配置类""" @@ -674,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/src/plugins/built_in/at_user_plugin/plugin.py b/src/plugins/built_in/at_user_plugin/plugin.py index 6d67b994c..1cac44fcc 100644 --- a/src/plugins/built_in/at_user_plugin/plugin.py +++ b/src/plugins/built_in/at_user_plugin/plugin.py @@ -28,9 +28,9 @@ class AtAction(BaseAction): # === 功能描述(必须填写)=== action_parameters = {"user_name": "需要艾特用户的名字", "at_message": "艾特用户时要发送的消息"} action_require = [ - "当需要艾特某个用户时使用", - "当你需要提醒特定用户查看消息时使用", - "在回复中需要明确指向某个用户时使用", + "当用户明确要求你去'叫'、'喊'、'提醒'或'艾特'某人时使用", + "当你判断,为了让特定的人看到消息,需要代表用户去呼叫他/她时使用", + "例如:'你去叫一下张三','提醒一下李四开会'", ] llm_judge_prompt = """ 判定是否需要使用艾特用户动作的条件: @@ -150,11 +150,16 @@ class AtAction(BaseAction): logger.error("回复器生成回复失败") return False, "回复生成失败" - final_message = llm_response.get("content", "") - if not final_message: + 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}, diff --git a/src/plugins/built_in/core_actions/emoji.py b/src/plugins/built_in/core_actions/emoji.py index 88f80e9f4..0e3305e6e 100644 --- a/src/plugins/built_in/core_actions/emoji.py +++ b/src/plugins/built_in/core_actions/emoji.py @@ -1,6 +1,5 @@ import random from typing import Tuple -from collections import deque # 导入新插件系统 from src.plugin_system import BaseAction, ActionActivationType, ChatMode @@ -10,7 +9,7 @@ from src.common.logger import get_logger # 导入API模块 - 标准Python包方式 from src.plugin_system.apis import llm_api, message_api -from src.chat.emoji_system.emoji_manager import get_emoji_manager +from src.chat.emoji_system.emoji_manager import get_emoji_manager, MaiEmoji from src.chat.utils.utils_image import image_path_to_base64 from src.config.config import global_config @@ -21,7 +20,6 @@ logger = get_logger("emoji") class EmojiAction(BaseAction): """表情动作 - 发送表情包""" - # --- 类级别属性 --- # 激活设置 if global_config.emoji.emoji_activate_type == "llm": activation_type = ActionActivationType.LLM_JUDGE @@ -35,9 +33,6 @@ class EmojiAction(BaseAction): # 动作基本信息 action_name = "emoji" action_description = "发送表情包辅助表达情绪" - - # 最近发送表情的历史记录 - _sent_emoji_history = deque(maxlen=4) # LLM判断提示词 llm_judge_prompt = """ @@ -72,12 +67,12 @@ class EmojiAction(BaseAction): reason = self.action_data.get("reason", "表达当前情绪") logger.info(f"{self.log_prefix} 发送表情原因: {reason}") - # 2. 获取所有表情包 + # 2. 获取所有有效的表情包对象 emoji_manager = get_emoji_manager() - all_emojis_obj = [e for e in emoji_manager.emoji_objects if not e.is_deleted] + all_emojis_obj: list[MaiEmoji] = [e for e in emoji_manager.emoji_objects if not e.is_deleted and e.description] if not all_emojis_obj: - logger.warning(f"{self.log_prefix} 无法获取任何表情包") - return False, "无法获取任何表情包" + logger.warning(f"{self.log_prefix} 无法获取任何带有描述的有效表情包") + return False, "无法获取任何带有描述的有效表情包" # 3. 准备情感数据和后备列表 emotion_map = {} @@ -90,29 +85,23 @@ class EmojiAction(BaseAction): desc = emoji.description emotions = emoji.emotion - # 使用 emoji 对象的 hash 作为唯一标识符 - all_emojis_data.append((b64, desc, emoji.hash)) + all_emojis_data.append((b64, desc)) for emo in emotions: if emo not in emotion_map: emotion_map[emo] = [] - emotion_map[emo].append((b64, desc, emoji.hash)) + 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()) - - chosen_emoji_b64, chosen_emoji_desc, chosen_emoji_hash = None, None, None + emoji_base64, emoji_description = "", "" if not available_emotions: logger.warning(f"{self.log_prefix} 获取到的表情包均无情感标签, 将随机发送") - # 随机选择一个不在历史记录中的表情 - selectable_emojis = [e for e in all_emojis_data if e[2] not in self._sent_emoji_history] - if not selectable_emojis: # 如果都发过了,就从全部里面随机选 - selectable_emojis = all_emojis_data - chosen_emoji_b64, chosen_emoji_desc, chosen_emoji_hash = random.choice(selectable_emojis) + 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) @@ -125,75 +114,62 @@ class EmojiAction(BaseAction): show_actions=False, ) - # 4. 构建prompt让LLM选择多个情感 + # 4. 构建prompt让LLM选择情感 prompt = f""" - 你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个情感标签列表中选择最匹配的 **3个** 情感标签,并按匹配度从高到低排序。 + 你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个情感标签列表中选择最匹配的一个。 这是最近的聊天记录: {messages_text} 这是理由:“{reason}” 这里是可用的情感标签:{available_emotions} - 请直接返回一个包含3个最匹配情感标签的有序列表,例如:["开心", "激动", "有趣"],不要进行任何解释或添加其他多余的文字。 + 请直接返回最匹配的那个情感标签,不要进行任何解释或添加其他多余的文字。 """ + 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} 未找到 'planner' 模型配置,无法调用LLM") - return False, "未找到 'planner' 模型配置" + logger.error(f"{self.log_prefix} 未找到'utils_small'模型配置,无法调用LLM") + return False, "未找到'utils_small'模型配置" - success, chosen_emotions_str, _, _ = await llm_api.generate_with_model( - prompt, model_config=chat_model_config, request_type="emoji_selection" + success, chosen_emotion, _, _ = await llm_api.generate_with_model( + prompt, model_config=chat_model_config, request_type="emoji" ) - selected_emoji_info = None - if success: - try: - # 解析LLM返回的列表 - import json - chosen_emotions = json.loads(chosen_emotions_str) - if isinstance(chosen_emotions, list): - logger.info(f"{self.log_prefix} LLM选择的情感候选项: {chosen_emotions}") - # 遍历候选项,找到第一个不在历史记录中的表情 - for emotion in chosen_emotions: - matched_key = next((key for key in emotion_map if emotion in key), None) - if matched_key: - # 从匹配到的表情中,随机选一个不在历史记录的 - candidate_emojis = [e for e in emotion_map[matched_key] if e[2] not in self._sent_emoji_history] - if candidate_emojis: - selected_emoji_info = random.choice(candidate_emojis) - break # 找到后立即跳出循环 - else: - logger.warning(f"{self.log_prefix} LLM返回的不是一个列表: {chosen_emotions_str}") - except (json.JSONDecodeError, TypeError): - logger.warning(f"{self.log_prefix} 解析LLM返回的情感列表失败: {chosen_emotions_str}") - - if selected_emoji_info: - chosen_emoji_b64, chosen_emoji_desc, chosen_emoji_hash = selected_emoji_info - logger.info(f"{self.log_prefix} 从候选项中选择表情: {chosen_emoji_desc}") + if not success: + logger.warning(f"{self.log_prefix} LLM调用失败: {chosen_emotion}, 将随机选择一个表情包") + emoji_base64, emoji_description = random.choice(all_emojis_data) else: - if not success: - logger.warning(f"{self.log_prefix} LLM调用失败, 将随机选择一个表情包") - else: - logger.warning(f"{self.log_prefix} 所有候选项均在最近发送历史中, 将随机选择") + 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) - selectable_emojis = [e for e in all_emojis_data if e[2] not in self._sent_emoji_history] - if not selectable_emojis: - selectable_emojis = all_emojis_data - chosen_emoji_b64, chosen_emoji_desc, chosen_emoji_hash = random.choice(selectable_emojis) + 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) - # 7. 发送表情包并更新历史记录 - if chosen_emoji_b64 and chosen_emoji_hash: - success = await self.send_emoji(chosen_emoji_b64) - if success: - self._sent_emoji_history.append(chosen_emoji_hash) - logger.info(f"{self.log_prefix} 表情包发送成功: {chosen_emoji_desc}") - logger.debug(f"{self.log_prefix} 最近表情历史: {list(self._sent_emoji_history)}") - return True, f"发送表情包: {chosen_emoji_desc}" + # 7. 发送表情包 + success = await self.send_emoji(emoji_base64) - logger.error(f"{self.log_prefix} 表情包发送失败") - return False, "表情包发送失败" + 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}" except Exception as e: logger.error(f"{self.log_prefix} 表情动作执行失败: {e}", exc_info=True) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 3a7ed77de..0570c4ec7 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.8.2" +version = "6.8.5" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -246,6 +246,7 @@ steal_emoji = true # 是否偷取表情包,让MoFox-Bot可以将一些表情 content_filtration = false # 是否启用表情包过滤,只有符合该要求的表情包才会被保存 filtration_prompt = "符合公序良俗" # 表情包过滤要求,只有符合该要求的表情包才会被保存 enable_emotion_analysis = false # 是否启用表情包感情关键词二次识别,启用后表情包在第一次识别完毕后将送入第二次大模型识别来总结感情关键词,并构建进回复和决策器的上下文消息中 +max_context_emojis = 30 # 每次随机传递给LLM的表情包详细描述的最大数量,0为全部 [memory] enable_memory = true # 是否启用记忆系统 @@ -321,6 +322,7 @@ word_replace_rate=0.006 # 整词替换概率 [response_splitter] enable = true # 是否启用回复分割器 +split_mode = "punctuation" # 分割模式: "llm" - 由语言模型决定, "punctuation" - 基于标点符号 max_length = 512 # 回复允许的最大长度 max_sentence_num = 8 # 回复允许的最大句子数 enable_kaomoji_protection = false # 是否启用颜文字保护 @@ -479,6 +481,9 @@ insomnia_duration_minutes = [30, 60] # 单次失眠状态的持续时间范围 # 入睡后,经过一段延迟后触发失眠判定的延迟时间(分钟),设置为范围以增加随机性 insomnia_trigger_delay_minutes = [15, 45] +[server] +host = "127.0.0.1" +port = 8080 [cross_context] # 跨群聊/私聊上下文共享配置 # 这是总开关,用于一键启用或禁用此功能