From 33606e70280e39e6178901a9e3611b8e48a8d467 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 24 Jul 2025 22:03:27 +0800 Subject: [PATCH 01/26] =?UTF-8?q?feat=20=E4=B8=BAfocus=E5=8A=A0=E5=85=A5?= =?UTF-8?q?=20mentioned=20bonus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/planner_actions/planner.py | 14 ++++++++++---- src/chat/replyer/default_generator.py | 9 ++++++++- src/config/config.py | 2 +- src/config/official_configs.py | 10 ++++++---- src/plugins/built_in/core_actions/reply.py | 7 +++++-- template/bot_config_template.toml | 6 +++--- 6 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 15eb7f9f..97b4a5fd 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -279,9 +279,15 @@ class ActionPlanner: self.last_obs_time_mark = time.time() if mode == ChatMode.FOCUS: + if global_config.normal_chat.mentioned_bot_inevitable_reply: + mentioned_bonus = "有人提到你" + if global_config.normal_chat.at_bot_inevitable_reply: + mentioned_bonus = "有人提到你,或者at你" + + by_what = "聊天内容" target_prompt = '\n "target_message_id":"触发action的消息id"' - no_action_block = """重要说明1: + no_action_block = f"""重要说明1: - 'no_reply' 表示只进行不进行回复,等待合适的回复时机 - 当你刚刚发送了消息,没有人回复时,选择no_reply - 当你一次发送了太多消息,为了避免打扰聊天节奏,选择no_reply @@ -289,13 +295,13 @@ class ActionPlanner: 动作:reply 动作描述:参与聊天回复,发送文本进行表达 - 你想要闲聊或者随便附和 -- 有人提到你 +- {mentioned_bonus} - 如果你刚刚进行了回复,不要对同一个话题重复回应 -{ +{{ "action": "reply", "target_message_id":"触发action的消息id", "reason":"回复的原因" -} +}} """ else: diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index ebdecb5c..925f721a 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -450,6 +450,9 @@ class DefaultReplyer: def _parse_reply_target(self, target_message: str) -> tuple: sender = "" target = "" + # 添加None检查,防止NoneType错误 + if target_message is None: + return sender, target if ":" in target_message or ":" in target_message: # 使用正则表达式匹配中文或英文冒号 parts = re.split(pattern=r"[::]", string=target_message, maxsplit=1) @@ -462,6 +465,10 @@ class DefaultReplyer: # 关键词检测与反应 keywords_reaction_prompt = "" try: + # 添加None检查,防止NoneType错误 + if target is None: + return keywords_reaction_prompt + # 处理关键词规则 for rule in global_config.keyword_reaction.keyword_rules: if any(keyword in target for keyword in rule.keywords): @@ -524,7 +531,7 @@ class DefaultReplyer: # 其他用户的对话 background_dialogue_list.append(msg_dict) except Exception as e: - logger.error(f"无法处理历史消息记录: {msg_dict}, 错误: {e}") + logger.error(f"![1753364551656](image/default_generator/1753364551656.png)记录: {msg_dict}, 错误: {e}") # 构建背景对话 prompt background_dialogue_prompt = "" diff --git a/src/config/config.py b/src/config/config.py index fae2ea2a..247775c5 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -49,7 +49,7 @@ TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "template") # 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 # 对该字段的更新,请严格参照语义化版本规范:https://semver.org/lang/zh-CN/ -MMC_VERSION = "0.9.0" +MMC_VERSION = "0.9.1-snapshot.1" def get_key_comment(toml_table, key): diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 1a14b47c..e19517f1 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -84,6 +84,12 @@ class ChatConfig(ConfigBase): use_s4u_prompt_mode: bool = False """是否使用 s4u 对话构建模式,该模式会分开处理当前对话对象和其他所有对话的内容进行 prompt 构建""" + mentioned_bot_inevitable_reply: bool = False + """提及 bot 必然回复""" + + at_bot_inevitable_reply: bool = False + """@bot 必然回复""" + # 修改:基于时段的回复频率配置,改为数组格式 time_based_talk_frequency: list[str] = field(default_factory=lambda: []) """ @@ -270,11 +276,7 @@ class NormalChatConfig(ConfigBase): response_interested_rate_amplifier: float = 1.0 """回复兴趣度放大系数""" - mentioned_bot_inevitable_reply: bool = False - """提及 bot 必然回复""" - at_bot_inevitable_reply: bool = False - """@bot 必然回复""" @dataclass diff --git a/src/plugins/built_in/core_actions/reply.py b/src/plugins/built_in/core_actions/reply.py index 644534db..d73337b2 100644 --- a/src/plugins/built_in/core_actions/reply.py +++ b/src/plugins/built_in/core_actions/reply.py @@ -32,13 +32,13 @@ class ReplyAction(BaseAction): # 动作基本信息 action_name = "reply" - action_description = "参与聊天回复,发送文本进行表达" + action_description = "" # 动作参数定义 action_parameters = {} # 动作使用场景 - action_require = ["你想要闲聊或者随便附和", "有人提到你", "如果你刚刚进行了回复,不要对同一个话题重复回应"] + action_require = [""] # 关联类型 associated_types = ["text"] @@ -46,6 +46,9 @@ class ReplyAction(BaseAction): def _parse_reply_target(self, target_message: str) -> tuple: sender = "" target = "" + # 添加None检查,防止NoneType错误 + if target_message is None: + return sender, target if ":" in target_message or ":" in target_message: # 使用正则表达式匹配中文或英文冒号 parts = re.split(pattern=r"[::]", string=target_message, maxsplit=1) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 9e83574e..b5678fb7 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -59,6 +59,9 @@ max_context_size = 25 # 上下文长度 thinking_timeout = 20 # 麦麦一次回复最长思考规划时间,超过这个时间的思考会放弃(往往是api反应太慢) replyer_random_probability = 0.5 # 首要replyer模型被选择的概率 +mentioned_bot_inevitable_reply = true # 提及 bot 大概率回复 +at_bot_inevitable_reply = true # @bot 或 提及bot 大概率回复 + use_s4u_prompt_mode = true # 是否使用 s4u 对话构建模式,该模式会更好的把握当前对话对象的对话内容,但是对群聊整理理解能力较差(测试功能!!可能有未知问题!!) @@ -101,11 +104,8 @@ ban_msgs_regex = [ ] [normal_chat] #普通聊天 -#一般回复参数 willing_mode = "classical" # 回复意愿模式 —— 经典模式:classical,mxp模式:mxp,自定义模式:custom(需要你自己实现) response_interested_rate_amplifier = 1 # 麦麦回复兴趣度放大系数 -mentioned_bot_inevitable_reply = true # 提及 bot 必然回复 -at_bot_inevitable_reply = true # @bot 必然回复(包含提及) [tool] enable_in_normal_chat = false # 是否在普通聊天中启用工具 From 16b125b815cd7974bc59ce2ac552d18e6c3481ac Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 24 Jul 2025 22:16:21 +0800 Subject: [PATCH 02/26] Update expression_learner.py --- src/chat/express/expression_learner.py | 100 +++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 7 deletions(-) diff --git a/src/chat/express/expression_learner.py b/src/chat/express/expression_learner.py index 4afcfe7d..ac41b12a 100644 --- a/src/chat/express/expression_learner.py +++ b/src/chat/express/expression_learner.py @@ -87,36 +87,90 @@ class ExpressionLearner: request_type="expressor.learner", ) self.llm_model = None + self._ensure_expression_directories() self._auto_migrate_json_to_db() self._migrate_old_data_create_date() + def _ensure_expression_directories(self): + """ + 确保表达方式相关的目录结构存在 + """ + base_dir = os.path.join("data", "expression") + directories_to_create = [ + base_dir, + os.path.join(base_dir, "learnt_style"), + os.path.join(base_dir, "learnt_grammar"), + ] + + for directory in directories_to_create: + try: + os.makedirs(directory, exist_ok=True) + logger.debug(f"确保目录存在: {directory}") + except Exception as e: + logger.error(f"创建目录失败 {directory}: {e}") + def _auto_migrate_json_to_db(self): """ 自动将/data/expression/learnt_style 和 learnt_grammar 下所有expressions.json迁移到数据库。 迁移完成后在/data/expression/done.done写入标记文件,存在则跳过。 """ - done_flag = os.path.join("data", "expression", "done.done") + base_dir = os.path.join("data", "expression") + done_flag = os.path.join(base_dir, "done.done") + + # 确保基础目录存在 + try: + os.makedirs(base_dir, exist_ok=True) + logger.debug(f"确保目录存在: {base_dir}") + except Exception as e: + logger.error(f"创建表达方式目录失败: {e}") + return + if os.path.exists(done_flag): logger.info("表达方式JSON已迁移,无需重复迁移。") return - base_dir = os.path.join("data", "expression") + + logger.info("开始迁移表达方式JSON到数据库...") + migrated_count = 0 + for type in ["learnt_style", "learnt_grammar"]: type_str = "style" if type == "learnt_style" else "grammar" type_dir = os.path.join(base_dir, type) if not os.path.exists(type_dir): + logger.debug(f"目录不存在,跳过: {type_dir}") continue - for chat_id in os.listdir(type_dir): + + try: + chat_ids = os.listdir(type_dir) + logger.debug(f"在 {type_dir} 中找到 {len(chat_ids)} 个聊天ID目录") + except Exception as e: + logger.error(f"读取目录失败 {type_dir}: {e}") + continue + + for chat_id in chat_ids: expr_file = os.path.join(type_dir, chat_id, "expressions.json") if not os.path.exists(expr_file): continue try: with open(expr_file, "r", encoding="utf-8") as f: expressions = json.load(f) + + if not isinstance(expressions, list): + logger.warning(f"表达方式文件格式错误,跳过: {expr_file}") + continue + for expr in expressions: + if not isinstance(expr, dict): + continue + situation = expr.get("situation") style_val = expr.get("style") count = expr.get("count", 1) last_active_time = expr.get("last_active_time", time.time()) + + if not situation or not style_val: + logger.warning(f"表达方式缺少必要字段,跳过: {expr}") + continue + # 查重:同chat_id+type+situation+style from src.common.database.database_model import Expression @@ -141,14 +195,28 @@ class ExpressionLearner: type=type_str, create_date=last_active_time, # 迁移时使用last_active_time作为创建时间 ) - logger.info(f"已迁移 {expr_file} 到数据库") + migrated_count += 1 + logger.info(f"已迁移 {expr_file} 到数据库,包含 {len(expressions)} 个表达方式") + except json.JSONDecodeError as e: + logger.error(f"JSON解析失败 {expr_file}: {e}") except Exception as e: logger.error(f"迁移表达方式 {expr_file} 失败: {e}") + # 标记迁移完成 try: + # 确保done.done文件的父目录存在 + done_parent_dir = os.path.dirname(done_flag) + if not os.path.exists(done_parent_dir): + os.makedirs(done_parent_dir, exist_ok=True) + logger.debug(f"为done.done创建父目录: {done_parent_dir}") + with open(done_flag, "w", encoding="utf-8") as f: f.write("done\n") - logger.info("表达方式JSON迁移已完成,已写入done.done标记文件") + logger.info(f"表达方式JSON迁移已完成,共迁移 {migrated_count} 个表达方式,已写入done.done标记文件") + except PermissionError as e: + logger.error(f"权限不足,无法写入done.done标记文件: {e}") + except OSError as e: + logger.error(f"文件系统错误,无法写入done.done标记文件: {e}") except Exception as e: logger.error(f"写入done.done标记文件失败: {e}") @@ -266,9 +334,17 @@ class ExpressionLearner: for type in ["style", "grammar"]: base_dir = os.path.join("data", "expression", f"learnt_{type}") if not os.path.exists(base_dir): + logger.debug(f"目录不存在,跳过衰减: {base_dir}") continue - for chat_id in os.listdir(base_dir): + try: + chat_ids = os.listdir(base_dir) + logger.debug(f"在 {base_dir} 中找到 {len(chat_ids)} 个聊天ID目录进行衰减") + except Exception as e: + logger.error(f"读取目录失败 {base_dir}: {e}") + continue + + for chat_id in chat_ids: file_path = os.path.join(base_dir, chat_id, "expressions.json") if not os.path.exists(file_path): continue @@ -277,14 +353,24 @@ class ExpressionLearner: with open(file_path, "r", encoding="utf-8") as f: expressions = json.load(f) + if not isinstance(expressions, list): + logger.warning(f"表达方式文件格式错误,跳过衰减: {file_path}") + continue + # 应用全局衰减 decayed_expressions = self.apply_decay_to_expressions(expressions, current_time) # 保存衰减后的结果 with open(file_path, "w", encoding="utf-8") as f: json.dump(decayed_expressions, f, ensure_ascii=False, indent=2) + + logger.debug(f"已对 {file_path} 应用衰减,剩余 {len(decayed_expressions)} 个表达方式") + except json.JSONDecodeError as e: + logger.error(f"JSON解析失败,跳过衰减 {file_path}: {e}") + except PermissionError as e: + logger.error(f"权限不足,无法更新 {file_path}: {e}") except Exception as e: - logger.error(f"全局衰减{type}表达方式失败: {e}") + logger.error(f"全局衰减{type}表达方式失败 {file_path}: {e}") continue learnt_style: Optional[List[Tuple[str, str, str]]] = [] From db896299bedbc0e84c25323c4301cf480846dffd Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 24 Jul 2025 22:30:27 +0800 Subject: [PATCH 03/26] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- changelogs/changelog.md | 8 ++++++++ src/chat/planner_actions/planner.py | 12 ++++++------ src/chat/utils/utils.py | 2 +- src/chat/willing/mode_classical.py | 2 +- src/config/config.py | 2 +- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 81bb4d08..3a9e14f8 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ ## 🔥 更新和安装 -**最新版本: v0.9.0** ([更新日志](changelogs/changelog.md)) +**最新版本: v0.9.1** ([更新日志](changelogs/changelog.md)) 可前往 [Release](https://github.com/MaiM-with-u/MaiBot/releases/) 页面下载最新版本 可前往 [启动器发布页面](https://github.com/MaiM-with-u/mailauncher/releases/)下载最新启动器 diff --git a/changelogs/changelog.md b/changelogs/changelog.md index e53ba6ed..c56426a7 100644 --- a/changelogs/changelog.md +++ b/changelogs/changelog.md @@ -1,5 +1,13 @@ # Changelog +## [0.9.1] - 2025-7-25 + +- 修复表达方式迁移空目录问题 +- 修复reply_to空字段问题 +- 将metioned bot 和 at应用到focus prompt中 + + + ## [0.9.0] - 2025-7-25 ### 摘要 diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 97b4a5fd..e3d1edef 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -279,10 +279,11 @@ class ActionPlanner: self.last_obs_time_mark = time.time() if mode == ChatMode.FOCUS: - if global_config.normal_chat.mentioned_bot_inevitable_reply: - mentioned_bonus = "有人提到你" - if global_config.normal_chat.at_bot_inevitable_reply: - mentioned_bonus = "有人提到你,或者at你" + 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你" by_what = "聊天内容" @@ -294,8 +295,7 @@ class ActionPlanner: 动作:reply 动作描述:参与聊天回复,发送文本进行表达 -- 你想要闲聊或者随便附和 -- {mentioned_bonus} +- 你想要闲聊或者随便附和{mentioned_bonus} - 如果你刚刚进行了回复,不要对同一个话题重复回应 {{ "action": "reply", diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index e7d2cadd..13ffc2fd 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -103,7 +103,7 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]: for nickname in nicknames: if nickname in message_content: is_mentioned = True - if is_mentioned and global_config.normal_chat.mentioned_bot_inevitable_reply: + if is_mentioned and global_config.chat.mentioned_bot_inevitable_reply: reply_probability = 1.0 logger.debug("被提及,回复概率设置为100%") return is_mentioned, reply_probability diff --git a/src/chat/willing/mode_classical.py b/src/chat/willing/mode_classical.py index e1527233..57400c44 100644 --- a/src/chat/willing/mode_classical.py +++ b/src/chat/willing/mode_classical.py @@ -35,7 +35,7 @@ class ClassicalWillingManager(BaseWillingManager): if interested_rate > 0.2: current_willing += interested_rate - 0.2 - if willing_info.is_mentioned_bot and global_config.normal_chat.mentioned_bot_inevitable_reply and current_willing < 2: + if willing_info.is_mentioned_bot and global_config.chat.mentioned_bot_inevitable_reply and current_willing < 2: current_willing += 1 if current_willing < 1.0 else 0.05 self.chat_reply_willing[chat_id] = min(current_willing, 1.0) diff --git a/src/config/config.py b/src/config/config.py index 247775c5..805a17d4 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -49,7 +49,7 @@ TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "template") # 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 # 对该字段的更新,请严格参照语义化版本规范:https://semver.org/lang/zh-CN/ -MMC_VERSION = "0.9.1-snapshot.1" +MMC_VERSION = "0.9.1" def get_key_comment(toml_table, key): From 8de3963069f3f02c6ddf4331b0554c79a67cdb9e Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 24 Jul 2025 22:47:13 +0800 Subject: [PATCH 04/26] =?UTF-8?q?feat=20=E7=BB=9F=E4=B8=80=E5=BF=83?= =?UTF-8?q?=E6=83=85=E9=85=8D=E7=BD=AE=EF=BC=8C=E4=B8=BArewartite=E5=8A=A0?= =?UTF-8?q?=E5=85=A5=E5=BF=83=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../heart_flow/heartflow_message_processor.py | 6 +++--- src/chat/replyer/default_generator.py | 18 +++++++++++++++--- src/config/official_configs.py | 12 +++--------- src/mood/mood_manager.py | 2 +- template/bot_config_template.toml | 4 +--- 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/chat/heart_flow/heartflow_message_processor.py b/src/chat/heart_flow/heartflow_message_processor.py index a9d11828..3aa174bb 100644 --- a/src/chat/heart_flow/heartflow_message_processor.py +++ b/src/chat/heart_flow/heartflow_message_processor.py @@ -111,9 +111,9 @@ class HeartFCMessageReceiver: subheartflow: SubHeartflow = await heartflow.get_or_create_subheartflow(chat.stream_id) # type: ignore # subheartflow.add_message_to_normal_chat_cache(message, interested_rate, is_mentioned) - - chat_mood = mood_manager.get_mood_by_chat_id(subheartflow.chat_id) - asyncio.create_task(chat_mood.update_mood_by_message(message, interested_rate)) + if global_config.mood.enable_mood: + chat_mood = mood_manager.get_mood_by_chat_id(subheartflow.chat_id) + asyncio.create_task(chat_mood.update_mood_by_message(message, interested_rate)) # 3. 日志记录 mes_name = chat.group_info.group_name if chat.group_info else "私聊" diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 925f721a..9d75671c 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -74,6 +74,7 @@ def init_prompt(): 你正在{chat_target_2},{reply_target_block} 对这句话,你想表达,原句:{raw_reply},原因是:{reason}。你现在要思考怎么组织回复 +你现在的心情是:{mood_state} 你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。请你修改你想表达的原句,符合你的表达风格和语言习惯 {config_expression_style},你可以完全重组回复,保留最基本的表达含义就好,但重组后保持语意通顺。 {keywords_reaction_prompt} @@ -620,9 +621,12 @@ class DefaultReplyer: is_group_chat = bool(chat_stream.group_info) reply_to = reply_data.get("reply_to", "none") extra_info_block = reply_data.get("extra_info", "") or reply_data.get("extra_info_block", "") - - chat_mood = mood_manager.get_mood_by_chat_id(chat_id) - mood_prompt = chat_mood.mood_state + + if global_config.mood.enable_mood: + chat_mood = mood_manager.get_mood_by_chat_id(chat_id) + mood_prompt = chat_mood.mood_state + else: + mood_prompt = "" sender, target = self._parse_reply_target(reply_to) @@ -883,6 +887,13 @@ class DefaultReplyer: reason = reply_data.get("reason", "") sender, target = self._parse_reply_target(reply_to) + # 添加情绪状态获取 + if global_config.mood.enable_mood: + chat_mood = mood_manager.get_mood_by_chat_id(chat_id) + mood_prompt = chat_mood.mood_state + else: + mood_prompt = "" + message_list_before_now_half = get_raw_msg_before_timestamp_with_chat( chat_id=chat_id, timestamp=time.time(), @@ -963,6 +974,7 @@ class DefaultReplyer: reply_target_block=reply_target_block, raw_reply=raw_reply, reason=reason, + mood_state=mood_prompt, # 添加情绪状态参数 config_expression_style=global_config.expression.expression_style, keywords_reaction_prompt=keywords_reaction_prompt, moderation_prompt=moderation_prompt_block, diff --git a/src/config/official_configs.py b/src/config/official_configs.py index e19517f1..82284d9b 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -408,15 +408,9 @@ class MoodConfig(ConfigBase): enable_mood: bool = False """是否启用情绪系统""" - - mood_update_interval: int = 1 - """情绪更新间隔(秒)""" - - mood_decay_rate: float = 0.95 - """情绪衰减率""" - - mood_intensity_factor: float = 0.7 - """情绪强度因子""" + + mood_update_threshold: float = 1.0 + """情绪更新阈值,越高,更新越慢""" @dataclass diff --git a/src/mood/mood_manager.py b/src/mood/mood_manager.py index 88c82792..38ed39bc 100644 --- a/src/mood/mood_manager.py +++ b/src/mood/mood_manager.py @@ -83,7 +83,7 @@ class ChatMood: logger.debug( f"base_probability: {base_probability}, time_multiplier: {time_multiplier}, interest_multiplier: {interest_multiplier}" ) - update_probability = min(1.0, base_probability * time_multiplier * interest_multiplier) + update_probability = global_config.mood.mood_update_threshold * min(1.0, base_probability * time_multiplier * interest_multiplier) if random.random() > update_probability: return diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index b5678fb7..ff8a79e7 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -148,9 +148,7 @@ enable_asr = false # 是否启用语音识别,启用后麦麦可以识别语 [mood] enable_mood = true # 是否启用情绪系统 -mood_update_interval = 1.0 # 情绪更新间隔 单位秒 -mood_decay_rate = 0.95 # 情绪衰减率 -mood_intensity_factor = 1.0 # 情绪强度因子 +mood_update_threshold = 1 # 情绪更新阈值,越高,更新越慢 [lpmm_knowledge] # lpmm知识库配置 enable = false # 是否启用lpmm知识库 From 4ab6d59a79135b88dadfb906470436b50decb9c5 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 24 Jul 2025 23:20:05 +0800 Subject: [PATCH 05/26] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8Denable=5Fth?= =?UTF-8?q?inking=E5=AF=BC=E8=87=B4=E7=9A=84400=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/utils/utils.py | 2 +- src/llm_models/utils_model.py | 126 +++++++++++++++++++++++++++++++--- 2 files changed, 117 insertions(+), 11 deletions(-) diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index 13ffc2fd..3ee4ae7b 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -78,7 +78,7 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]: # print(f"is_mentioned: {is_mentioned}") # print(f"is_at: {is_at}") - if is_at and global_config.normal_chat.at_bot_inevitable_reply: + if is_at and global_config.chat.at_bot_inevitable_reply: reply_probability = 1.0 logger.debug("被@,回复概率设置为100%") else: diff --git a/src/llm_models/utils_model.py b/src/llm_models/utils_model.py index 3621b450..8a121588 100644 --- a/src/llm_models/utils_model.py +++ b/src/llm_models/utils_model.py @@ -109,10 +109,15 @@ class LLMRequest: def __init__(self, model: dict, **kwargs): # 将大写的配置键转换为小写并从config中获取实际值 + logger.debug(f"🔍 [模型初始化] 开始初始化模型: {model.get('name', 'Unknown')}") + logger.debug(f"🔍 [模型初始化] 模型配置: {model}") + logger.debug(f"🔍 [模型初始化] 额外参数: {kwargs}") + try: # print(f"model['provider']: {model['provider']}") self.api_key = os.environ[f"{model['provider']}_KEY"] self.base_url = os.environ[f"{model['provider']}_BASE_URL"] + logger.debug(f"🔍 [模型初始化] 成功获取环境变量: {model['provider']}_KEY 和 {model['provider']}_BASE_URL") except AttributeError as e: logger.error(f"原始 model dict 信息:{model}") logger.error(f"配置错误:找不到对应的配置项 - {str(e)}") @@ -124,6 +129,10 @@ class LLMRequest: self.model_name: str = model["name"] self.params = kwargs + # 记录配置文件中声明了哪些参数(不管值是什么) + self.has_enable_thinking = "enable_thinking" in model + self.has_thinking_budget = "thinking_budget" in model + self.enable_thinking = model.get("enable_thinking", False) self.temp = model.get("temp", 0.7) self.thinking_budget = model.get("thinking_budget", 4096) @@ -132,12 +141,24 @@ class LLMRequest: self.pri_out = model.get("pri_out", 0) self.max_tokens = model.get("max_tokens", global_config.model.model_max_output_length) # print(f"max_tokens: {self.max_tokens}") + + logger.debug(f"🔍 [模型初始化] 模型参数设置完成:") + logger.debug(f" - model_name: {self.model_name}") + logger.debug(f" - has_enable_thinking: {self.has_enable_thinking}") + logger.debug(f" - enable_thinking: {self.enable_thinking}") + logger.debug(f" - has_thinking_budget: {self.has_thinking_budget}") + logger.debug(f" - thinking_budget: {self.thinking_budget}") + logger.debug(f" - temp: {self.temp}") + logger.debug(f" - stream: {self.stream}") + logger.debug(f" - max_tokens: {self.max_tokens}") + logger.debug(f" - base_url: {self.base_url}") # 获取数据库实例 self._init_database() # 从 kwargs 中提取 request_type,如果没有提供则默认为 "default" self.request_type = kwargs.pop("request_type", "default") + logger.debug(f"🔍 [模型初始化] 初始化完成,request_type: {self.request_type}") @staticmethod def _init_database(): @@ -262,11 +283,12 @@ class LLMRequest: if self.temp != 0.7: payload["temperature"] = self.temp - # 添加enable_thinking参数(如果不是默认值False) - if not self.enable_thinking: - payload["enable_thinking"] = False + # 添加enable_thinking参数(只有配置文件中声明了才添加,不管值是true还是false) + if self.has_enable_thinking: + payload["enable_thinking"] = self.enable_thinking - if self.thinking_budget != 4096: + # 添加thinking_budget参数(只有配置文件中声明了才添加) + if self.has_thinking_budget: payload["thinking_budget"] = self.thinking_budget if self.max_tokens: @@ -334,6 +356,19 @@ class LLMRequest: # 似乎是openai流式必须要的东西,不过阿里云的qwq-plus加了这个没有影响 if request_content["stream_mode"]: headers["Accept"] = "text/event-stream" + + # 添加请求发送前的调试信息 + logger.debug(f"🔍 [请求调试] 模型 {self.model_name} 准备发送请求") + logger.debug(f"🔍 [请求调试] API URL: {request_content['api_url']}") + logger.debug(f"🔍 [请求调试] 请求头: {await self._build_headers(no_key=True, is_formdata=file_bytes is not None)}") + + if not file_bytes: + # 安全地记录请求体(隐藏敏感信息) + safe_payload = await _safely_record(request_content, request_content["payload"]) + logger.debug(f"🔍 [请求调试] 请求体: {json.dumps(safe_payload, indent=2, ensure_ascii=False)}") + else: + logger.debug(f"🔍 [请求调试] 文件上传请求,文件格式: {request_content['file_format']}") + async with aiohttp.ClientSession(connector=await get_tcp_connector()) as session: post_kwargs = {"headers": headers} # form-data数据上传方式不同 @@ -491,7 +526,36 @@ class LLMRequest: logger.warning(f"模型 {self.model_name} 请求限制(429),等待{wait_time}秒后重试...") raise RuntimeError("请求限制(429)") elif response.status in policy["abort_codes"]: - if response.status != 403: + # 特别处理400错误,添加详细调试信息 + if response.status == 400: + logger.error(f"🔍 [调试信息] 模型 {self.model_name} 参数错误 (400) - 开始详细诊断") + logger.error(f"🔍 [调试信息] 模型名称: {self.model_name}") + logger.error(f"🔍 [调试信息] API地址: {self.base_url}") + logger.error(f"🔍 [调试信息] 模型配置参数:") + logger.error(f" - enable_thinking: {self.enable_thinking}") + logger.error(f" - temp: {self.temp}") + logger.error(f" - thinking_budget: {self.thinking_budget}") + logger.error(f" - stream: {self.stream}") + logger.error(f" - max_tokens: {self.max_tokens}") + logger.error(f" - pri_in: {self.pri_in}") + logger.error(f" - pri_out: {self.pri_out}") + logger.error(f"🔍 [调试信息] 原始params: {self.params}") + + # 尝试获取服务器返回的详细错误信息 + try: + error_text = await response.text() + logger.error(f"🔍 [调试信息] 服务器返回的原始错误内容: {error_text}") + + try: + error_json = json.loads(error_text) + logger.error(f"🔍 [调试信息] 解析后的错误JSON: {json.dumps(error_json, indent=2, ensure_ascii=False)}") + except json.JSONDecodeError: + logger.error(f"🔍 [调试信息] 错误响应不是有效的JSON格式") + except Exception as e: + logger.error(f"🔍 [调试信息] 无法读取错误响应内容: {str(e)}") + + raise RequestAbortException("参数错误,请检查调试信息", response) + elif response.status != 403: raise RequestAbortException("请求出现错误,中断处理", response) else: raise PermissionDeniedException("模型禁止访问") @@ -510,6 +574,19 @@ class LLMRequest: logger.error( f"模型 {self.model_name} 错误码: {response.status} - {error_code_mapping.get(response.status)}" ) + + # 如果是400错误,额外输出请求体信息用于调试 + if response.status == 400: + logger.error(f"🔍 [异常调试] 400错误 - 请求体调试信息:") + try: + safe_payload = await _safely_record(request_content, payload) + logger.error(f"🔍 [异常调试] 发送的请求体: {json.dumps(safe_payload, indent=2, ensure_ascii=False)}") + except Exception as debug_error: + logger.error(f"🔍 [异常调试] 无法安全记录请求体: {str(debug_error)}") + logger.error(f"🔍 [异常调试] 原始payload类型: {type(payload)}") + if isinstance(payload, dict): + logger.error(f"🔍 [异常调试] 原始payload键: {list(payload.keys())}") + # print(request_content) # print(response) # 尝试获取并记录服务器返回的详细错误信息 @@ -654,14 +731,27 @@ class LLMRequest: """ # 复制一份参数,避免直接修改原始数据 new_params = dict(params) + + logger.debug(f"🔍 [参数转换] 模型 {self.model_name} 开始参数转换") + logger.debug(f"🔍 [参数转换] 是否为CoT模型: {self.model_name.lower() in self.MODELS_NEEDING_TRANSFORMATION}") + logger.debug(f"🔍 [参数转换] CoT模型列表: {self.MODELS_NEEDING_TRANSFORMATION}") if self.model_name.lower() in self.MODELS_NEEDING_TRANSFORMATION: + logger.debug(f"🔍 [参数转换] 检测到CoT模型,开始参数转换") # 删除 'temperature' 参数(如果存在),但避免删除我们在_build_payload中添加的自定义温度 if "temperature" in new_params and new_params["temperature"] == 0.7: - new_params.pop("temperature") + removed_temp = new_params.pop("temperature") + logger.debug(f"🔍 [参数转换] 移除默认temperature参数: {removed_temp}") # 如果存在 'max_tokens',则重命名为 'max_completion_tokens' if "max_tokens" in new_params: + old_value = new_params["max_tokens"] new_params["max_completion_tokens"] = new_params.pop("max_tokens") + logger.debug(f"🔍 [参数转换] 参数重命名: max_tokens({old_value}) -> max_completion_tokens({new_params['max_completion_tokens']})") + else: + logger.debug(f"🔍 [参数转换] 非CoT模型,无需参数转换") + + logger.debug(f"🔍 [参数转换] 转换前参数: {params}") + logger.debug(f"🔍 [参数转换] 转换后参数: {new_params}") return new_params async def _build_formdata_payload(self, file_bytes: bytes, file_format: str) -> aiohttp.FormData: @@ -693,7 +783,12 @@ class LLMRequest: async def _build_payload(self, prompt: str, image_base64: str = None, image_format: str = None) -> dict: """构建请求体""" # 复制一份参数,避免直接修改 self.params + logger.debug(f"🔍 [参数构建] 模型 {self.model_name} 开始构建请求体") + logger.debug(f"🔍 [参数构建] 原始self.params: {self.params}") + params_copy = await self._transform_parameters(self.params) + logger.debug(f"🔍 [参数构建] 转换后的params_copy: {params_copy}") + if image_base64: messages = [ { @@ -715,26 +810,37 @@ class LLMRequest: "messages": messages, **params_copy, } + + logger.debug(f"🔍 [参数构建] 基础payload构建完成: {list(payload.keys())}") # 添加temp参数(如果不是默认值0.7) if self.temp != 0.7: payload["temperature"] = self.temp + logger.debug(f"🔍 [参数构建] 添加temperature参数: {self.temp}") - # 添加enable_thinking参数(如果不是默认值False) - if not self.enable_thinking: - payload["enable_thinking"] = False + # 添加enable_thinking参数(只有配置文件中声明了才添加,不管值是true还是false) + if self.has_enable_thinking: + payload["enable_thinking"] = self.enable_thinking + logger.debug(f"🔍 [参数构建] 添加enable_thinking参数: {self.enable_thinking}") - if self.thinking_budget != 4096: + # 添加thinking_budget参数(只有配置文件中声明了才添加) + if self.has_thinking_budget: payload["thinking_budget"] = self.thinking_budget + logger.debug(f"🔍 [参数构建] 添加thinking_budget参数: {self.thinking_budget}") if self.max_tokens: payload["max_tokens"] = self.max_tokens + logger.debug(f"🔍 [参数构建] 添加max_tokens参数: {self.max_tokens}") # if "max_tokens" not in payload and "max_completion_tokens" not in payload: # payload["max_tokens"] = global_config.model.model_max_output_length # 如果 payload 中依然存在 max_tokens 且需要转换,在这里进行再次检查 if self.model_name.lower() in self.MODELS_NEEDING_TRANSFORMATION and "max_tokens" in payload: + old_value = payload["max_tokens"] payload["max_completion_tokens"] = payload.pop("max_tokens") + logger.debug(f"🔍 [参数构建] CoT模型参数转换: max_tokens({old_value}) -> max_completion_tokens({payload['max_completion_tokens']})") + + logger.debug(f"🔍 [参数构建] 最终payload键列表: {list(payload.keys())}") return payload def _default_response_handler( From a82de0a50e264f8ba9912862fdf7a9b9f461d6a9 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Fri, 25 Jul 2025 00:08:00 +0800 Subject: [PATCH 06/26] =?UTF-8?q?action=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plugins/action-components.md | 548 +++++++++++--------------- src/plugin_system/base/base_action.py | 20 +- 2 files changed, 234 insertions(+), 334 deletions(-) diff --git a/docs/plugins/action-components.md b/docs/plugins/action-components.md index d68d8707..3953c79c 100644 --- a/docs/plugins/action-components.md +++ b/docs/plugins/action-components.md @@ -4,42 +4,183 @@ Action是给麦麦在回复之外提供额外功能的智能组件,**由麦麦的决策系统自主选择是否使用**,具有随机性和拟人化的调用特点。Action不是直接响应用户命令,而是让麦麦根据聊天情境智能地选择合适的动作,使其行为更加自然和真实。 -### 🎯 Action的特点 +### Action的特点 - 🧠 **智能激活**:麦麦根据多种条件智能判断是否使用 -- 🎲 **随机性**:增加行为的不可预测性,更接近真人交流 +- 🎲 **可随机性**:可以使用随机数激活,增加行为的不可预测性,更接近真人交流 - 🤖 **拟人化**:让麦麦的回应更自然、更有个性 - 🔄 **情境感知**:基于聊天上下文做出合适的反应 -## 🎯 两层决策机制 +--- + +## 🎯 Action组件的基本结构 +首先,所有的Action都应该继承`BaseAction`类。 + +其次,每个Action组件都应该实现以下基本信息: +```python +class ExampleAction(BaseAction): + action_name = "example_action" # 动作的唯一标识符 + action_description = "这是一个示例动作" # 动作描述 + activation_type = ActionActivationType.ALWAYS # 这里以 ALWAYS 为例 + mode_enable = ChatMode.ALL # 这里以 ALL 为例 + associated_types = ["text", "emoji", ...] # 关联类型 + parallel_action = False # 是否允许与其他Action并行执行 + action_parameters = {"param1": "参数1的说明", "param2": "参数2的说明", ...} + # Action使用场景描述 - 帮助LLM判断何时"选择"使用 + action_require = ["使用场景描述1", "使用场景描述2", ...] + + async def execute(self) -> Tuple[bool, str]: + """ + 执行Action的主要逻辑 + + Returns: + Tuple[bool, str]: (是否成功, 执行结果描述) + """ + # ---- 执行动作的逻辑 ---- + return True, "执行成功" +``` +#### associated_types: 该Action会发送的消息类型,例如文本、表情等。 + +这部分由Adapter传递给处理器。 + +以 MaiBot-Napcat-Adapter 为例,可选项目如下: +| 类型 | 说明 | 格式 | +| --- | --- | --- | +| text | 文本消息 | str | +| emoji | 表情消息 | str: 表情包的无头base64| +| image | 图片消息 | str: 图片的无头base64 | +| reply | 回复消息 | str: 回复的消息ID | +| voice | 语音消息 | str: wav格式语音的无头base64 | +| command | 命令消息 | 参见Adapter文档 | +| voiceurl | 语音URL消息 | str: wav格式语音的URL | +| music | 音乐消息 | str: 这首歌在网易云音乐的音乐id | +| videourl | 视频URL消息 | str: 视频的URL | +| file | 文件消息 | str: 文件的路径 | + +**请知悉,对于不同的处理器,其支持的消息类型可能会有所不同。在开发时请注意。** + +#### action_parameters: 该Action的参数说明。 +这是一个字典,键为参数名,值为参数说明。这个字段可以帮助LLM理解如何使用这个Action,并由LLM返回对应的参数,最后传递到 Action 的 action_data 属性中。其格式与你定义的格式完全相同 **(除非LLM哈气了,返回了错误的内容)**。 + +--- + +## 🎯 Action 调用的决策机制 Action采用**两层决策机制**来优化性能和决策质量: -### 第一层:激活控制(Activation Control) +> 设计目的:在加载许多插件的时候降低LLM决策压力,避免让麦麦在过多的选项中纠结。 -**激活决定麦麦是否"知道"这个Action的存在**,即这个Action是否进入决策候选池。**不被激活的Action麦麦永远不会选择**。 +**第一层:激活控制(Activation Control)** -> 🎯 **设计目的**:在加载许多插件的时候降低LLM决策压力,避免让麦麦在过多的选项中纠结。 +激活决定麦麦是否 **“知道”** 这个Action的存在,即这个Action是否进入决策候选池。不被激活的Action麦麦永远不会选择。 -#### 激活类型说明 +**第二层:使用决策(Usage Decision)** -| 激活类型 | 说明 | 使用场景 | -| ------------- | ------------------------------------------- | ------------------------ | -| `NEVER` | 从不激活,Action对麦麦不可见 | 临时禁用某个Action | -| `ALWAYS` | 永远激活,Action总是在麦麦的候选池中 | 核心功能,如回复、不回复 | -| `LLM_JUDGE` | 通过LLM智能判断当前情境是否需要激活此Action | 需要智能判断的复杂场景 | -| `RANDOM` | 基于随机概率决定是否激活 | 增加行为随机性的功能 | -| `KEYWORD` | 当检测到特定关键词时激活 | 明确触发条件的功能 | +在Action被激活后,使用条件决定麦麦什么时候会 **“选择”** 使用这个Action。 -#### 聊天模式控制 +### 决策参数详解 🔧 -| 模式 | 说明 | -| ------------------- | ------------------------ | -| `ChatMode.FOCUS` | 仅在专注聊天模式下可激活 | -| `ChatMode.NORMAL` | 仅在普通聊天模式下可激活 | -| `ChatMode.ALL` | 所有模式下都可激活 | +#### 第一层:ActivationType 激活类型说明 -### 第二层:使用决策(Usage Decision) +| 激活类型 | 说明 | 使用场景 | +| ----------- | ---------------------------------------- | ---------------------- | +| [`NEVER`](#never-激活) | 从不激活,Action对麦麦不可见 | 临时禁用某个Action | +| [`ALWAYS`](#always-激活) | 永远激活,Action总是在麦麦的候选池中 | 核心功能,如回复、不回复 | +| [`LLM_JUDGE`](#llm_judge-激活) | 通过LLM智能判断当前情境是否需要激活此Action | 需要智能判断的复杂场景 | +| `RANDOM` | 基于随机概率决定是否激活 | 增加行为随机性的功能 | +| `KEYWORD` | 当检测到特定关键词时激活 | 明确触发条件的功能 | + +#### `NEVER` 激活 + +`ActionActivationType.NEVER` 会使得 Action 永远不会被激活 + +```python +class DisabledAction(BaseAction): + activation_type = ActionActivationType.NEVER # 永远不激活 + + async def execute(self) -> Tuple[bool, str]: + # 这个Action永远不会被执行 + return False, "这个Action被禁用" +``` + +#### `ALWAYS` 激活 + +`ActionActivationType.ALWAYS` 会使得 Action 永远会被激活,即一直在 Action 候选池中 + +这种激活方式常用于核心功能,如回复或不回复。 + +```python +class AlwaysActivatedAction(BaseAction): + activation_type = ActionActivationType.ALWAYS # 永远激活 + + async def execute(self) -> Tuple[bool, str]: + # 执行核心功能 + return True, "执行了核心功能" +``` + +#### `LLM_JUDGE` 激活 + +`ActionActivationType.LLM_JUDGE`会使得这个 Action 根据 LLM 的判断来决定是否加入候选池。 + +而 LLM 的判断是基于代码中预设的`llm_judge_prompt`和自动提供的聊天上下文进行的。 + +因此使用此种方法需要实现`llm_judge_prompt`属性。 + +```python +class LLMJudgedAction(BaseAction): + activation_type = ActionActivationType.LLM_JUDGE # 通过LLM判断激活 + # LLM判断提示词 + llm_judge_prompt = ( + "判定是否需要使用这个动作的条件:\n" + "1. 用户希望调用XXX这个动作\n" + "...\n" + "请回答\"是\"或\"否\"。\n" + ) + + async def execute(self) -> Tuple[bool, str]: + # 根据LLM判断是否执行 + return True, "执行了LLM判断功能" +``` + +#### `RANDOM` 激活 + +`ActionActivationType.RANDOM`会使得这个 Action 根据随机概率决定是否加入候选池。 + +概率则由代码中的`random_activation_probability`控制。在内部实现中我们使用了`random.random()`来生成一个0到1之间的随机数,并与这个概率进行比较。 + +因此使用这个方法需要实现`random_activation_probability`属性。 + +```python +class SurpriseAction(BaseAction): + activation_type = ActionActivationType.RANDOM # 基于随机概率激活 + # 随机激活概率 + random_activation_probability = 0.1 # 10%概率激活 + + async def execute(self) -> Tuple[bool, str]: + # 执行惊喜动作 + return True, "发送了惊喜内容" +``` + +#### `KEYWORD` 激活 + +`ActionActivationType.KEYWORD`会使得这个 Action 在检测到特定关键词时激活。 + +关键词由代码中的`activation_keywords`定义,而`keyword_case_sensitive`则控制关键词匹配时是否区分大小写。在内部实现中,我们使用了`in`操作符来检查消息内容是否包含这些关键词。 + +因此,使用此种方法需要实现`activation_keywords`和`keyword_case_sensitive`属性。 + +```python +class GreetingAction(BaseAction): + activation_type = ActionActivationType.KEYWORD # 关键词激活 + activation_keywords = ["你好", "hello", "hi", "嗨"] # 关键词配置 + keyword_case_sensitive = False # 不区分大小写 + + async def execute(self) -> Tuple[bool, str]: + # 执行问候逻辑 + return True, "发送了问候" +``` + +#### 第二层:使用决策 **在Action被激活后,使用条件决定麦麦什么时候会"选择"使用这个Action**。 @@ -49,17 +190,16 @@ Action采用**两层决策机制**来优化性能和决策质量: - `action_parameters`:所需参数,影响Action的可执行性 - 当前聊天上下文和麦麦的决策逻辑 -### 🎬 决策流程示例 +--- -假设有一个"发送表情"Action: +### 决策流程示例 ```python class EmojiAction(BaseAction): # 第一层:激活控制 - focus_activation_type = ActionActivationType.RANDOM # 专注模式下随机激活 - normal_activation_type = ActionActivationType.KEYWORD # 普通模式下关键词激活 - activation_keywords = ["表情", "emoji", "😊"] - + activation_type = ActionActivationType.RANDOM # 随机激活 + random_activation_probability = 0.1 # 10%概率激活 + # 第二层:使用决策 action_require = [ "表达情绪时可以选择使用", @@ -72,311 +212,85 @@ class EmojiAction(BaseAction): 1. **第一层激活判断**: - - 普通模式:只有当用户消息包含"表情"、"emoji"或"😊"时,麦麦才"知道"可以使用这个Action - - 专注模式:随机激活,有概率让麦麦"看到"这个Action + - 使用随机数进行决策,当`random.random() < self.random_activation_probability`时,麦麦才"知道"可以使用这个Action 2. **第二层使用决策**: - - 即使Action被激活,麦麦还会根据 `action_require`中的条件判断是否真正选择使用 + - 即使Action被激活,麦麦还会根据 `action_require` 中的条件判断是否真正选择使用 - 例如:如果刚刚已经发过表情,根据"不要连续发送多个表情"的要求,麦麦可能不会选择这个Action -## 📋 Action必须项清单 - -每个Action类都**必须**包含以下属性: - -### 1. 激活控制必须项 +--- +## Action 内置属性说明 ```python -# 专注模式下的激活类型 -focus_activation_type = ActionActivationType.LLM_JUDGE - -# 普通模式下的激活类型 -normal_activation_type = ActionActivationType.KEYWORD - -# 启用的聊天模式 -mode_enable = ChatMode.ALL - -# 是否允许与其他Action并行执行 -parallel_action = False -``` - -### 2. 基本信息必须项 - -```python -# Action的唯一标识名称 -action_name = "my_action" - -# Action的功能描述 -action_description = "描述这个Action的具体功能和用途" -``` - -### 3. 功能定义必须项 - -```python -# Action参数定义 - 告诉LLM执行时需要什么参数 -action_parameters = { - "param1": "参数1的说明", - "param2": "参数2的说明" -} - -# Action使用场景描述 - 帮助LLM判断何时"选择"使用 -action_require = [ - "使用场景描述1", - "使用场景描述2" -] - -# 关联的消息类型 - 说明Action能处理什么类型的内容 -associated_types = ["text", "emoji", "image"] -``` - -### 4. 执行方法必须项 - -```python -async def execute(self) -> Tuple[bool, str]: - """ - 执行Action的主要逻辑 - - Returns: - Tuple[bool, str]: (是否成功, 执行结果描述) - """ - # 执行动作的代码 - success = True - message = "动作执行成功" - - return success, message -``` - -## 🔧 激活类型详解 - -### KEYWORD激活 - -当检测到特定关键词时激活Action: - -```python -class GreetingAction(BaseAction): - focus_activation_type = ActionActivationType.KEYWORD - normal_activation_type = ActionActivationType.KEYWORD - - # 关键词配置 - activation_keywords = ["你好", "hello", "hi", "嗨"] - keyword_case_sensitive = False # 不区分大小写 - - async def execute(self) -> Tuple[bool, str]: - # 执行问候逻辑 - return True, "发送了问候" -``` - -### LLM_JUDGE激活 - -通过LLM智能判断是否激活: - -```python -class HelpAction(BaseAction): - focus_activation_type = ActionActivationType.LLM_JUDGE - normal_activation_type = ActionActivationType.LLM_JUDGE - - # LLM判断提示词 - llm_judge_prompt = """ - 判定是否需要使用帮助动作的条件: - 1. 用户表达了困惑或需要帮助 - 2. 用户提出了问题但没有得到满意答案 - 3. 对话中出现了技术术语或复杂概念 - - 请回答"是"或"否"。 - """ - - async def execute(self) -> Tuple[bool, str]: - # 执行帮助逻辑 - return True, "提供了帮助" -``` - -### RANDOM激活 - -基于随机概率激活: - -```python -class SurpriseAction(BaseAction): - focus_activation_type = ActionActivationType.RANDOM - normal_activation_type = ActionActivationType.RANDOM - - # 随机激活概率 - random_activation_probability = 0.1 # 10%概率激活 - - async def execute(self) -> Tuple[bool, str]: - # 执行惊喜动作 - return True, "发送了惊喜内容" -``` - -### ALWAYS激活 - -永远激活,常用于核心功能: - -```python -class CoreAction(BaseAction): - focus_activation_type = ActionActivationType.ALWAYS - normal_activation_type = ActionActivationType.ALWAYS - - async def execute(self) -> Tuple[bool, str]: - # 执行核心功能 - return True, "执行了核心功能" -``` - -### NEVER激活 - -从不激活,用于临时禁用: - -```python -class DisabledAction(BaseAction): - focus_activation_type = ActionActivationType.NEVER - normal_activation_type = ActionActivationType.NEVER - - async def execute(self) -> Tuple[bool, str]: - # 这个方法不会被调用 - return False, "已禁用" -``` - -## 📚 BaseAction内置属性和方法 - -### 内置属性 - -```python -class MyAction(BaseAction): +class BaseAction: def __init__(self): # 消息相关属性 - self.message # 当前消息对象 - self.chat_stream # 聊天流对象 - self.user_id # 用户ID - self.user_nickname # 用户昵称 - self.platform # 平台类型 (qq, telegram等) - self.chat_id # 聊天ID - self.is_group # 是否群聊 - - # Action相关属性 - self.action_data # Action执行时的数据 - self.thinking_id # 思考ID - self.matched_groups # 匹配到的组(如果有正则匹配) -``` + self.log_prefix: str # 日志前缀 + self.group_id: str # 群组ID + self.group_name: str # 群组名称 + self.user_id: str # 用户ID + self.user_nickname: str # 用户昵称 + self.platform: str # 平台类型 (qq, telegram等) + self.chat_id: str # 聊天ID + self.chat_stream: ChatStream # 聊天流对象 + self.is_group: bool # 是否群聊 -### 内置方法 + # 消息体 + self.action_message: dict # 消息数据 + + # Action相关属性 + self.action_data: dict # Action执行时的数据 + self.thinking_id: str # 思考ID +``` +action_message为一个字典,包含的键值对如下(省略了不必要的键值对) ```python -class MyAction(BaseAction): +{ + "message_id": "1234567890", # 消息id,str + "time": 1627545600.0, # 时间戳,float + "chat_id": "abcdef123456", # 聊天ID,str + "reply_to": None, # 回复消息id,str或None + "interest_value": 0.85, # 兴趣值,float + "is_mentioned": True, # 是否被提及,bool + "chat_info_last_active_time": 1627548600.0, # 最后活跃时间,float + "processed_plain_text": None, # 处理后的文本,str或None + "additional_config": None, # Adapter传来的additional_config,dict或None + "is_emoji": False, # 是否为表情,bool + "is_picid": False, # 是否为图片ID,bool + "is_command": False # 是否为命令,bool +} +``` + +部分值的格式请自行查询数据库。 + +--- + +## Action 内置方法说明 +```python +class BaseAction: # 配置相关 def get_config(self, key: str, default=None): - """获取配置值""" - pass + """获取插件配置值,使用嵌套键访问""" - # 消息发送相关 - async def send_text(self, text: str): + async def wait_for_new_message(self, timeout: int = 1200) -> Tuple[bool, str]: + """等待新消息或超时""" + + async def send_text(self, content: str, reply_to: str = "", reply_to_platform_id: str = "", typing: bool = False) -> bool: """发送文本消息""" - pass - - async def send_emoji(self, emoji_base64: str): + + async def send_emoji(self, emoji_base64: str) -> bool: """发送表情包""" - pass - - async def send_image(self, image_base64: str): + + async def send_image(self, image_base64: str) -> bool: """发送图片""" - pass - - # 动作记录相关 - async def store_action_info(self, **kwargs): - """记录动作信息""" - pass + + async def send_custom(self, message_type: str, content: str, typing: bool = False, reply_to: str = "") -> bool: + """发送自定义类型消息""" + + async def store_action_info(self, action_build_into_prompt: bool = False, action_prompt_display: str = "", action_done: bool = True) -> None: + """存储动作信息到数据库""" + + async def send_command(self, command_name: str, args: Optional[dict] = None, display_message: str = "", storage_message: bool = True) -> bool: + """发送命令消息""" ``` - -## 🎯 完整Action示例 - -```python -from src.plugin_system import BaseAction, ActionActivationType, ChatMode -from typing import Tuple - -class ExampleAction(BaseAction): - """示例Action - 展示完整的Action结构""" - - # === 激活控制 === - focus_activation_type = ActionActivationType.LLM_JUDGE - normal_activation_type = ActionActivationType.KEYWORD - mode_enable = ChatMode.ALL - parallel_action = False - - # 关键词激活配置 - activation_keywords = ["示例", "测试", "example"] - keyword_case_sensitive = False - - # LLM判断提示词 - llm_judge_prompt = "当用户需要示例或测试功能时激活" - - # 随机激活概率(如果使用RANDOM类型) - random_activation_probability = 0.2 - - # === 基本信息 === - action_name = "example_action" - action_description = "这是一个示例Action,用于演示Action的完整结构" - - # === 功能定义 === - action_parameters = { - "content": "要处理的内容", - "type": "处理类型", - "options": "可选配置" - } - - action_require = [ - "用户需要示例功能时使用", - "适合用于测试和演示", - "不要在正式对话中频繁使用" - ] - - associated_types = ["text", "emoji"] - - async def execute(self) -> Tuple[bool, str]: - """执行示例Action""" - try: - # 获取Action参数 - content = self.action_data.get("content", "默认内容") - action_type = self.action_data.get("type", "default") - - # 获取配置 - enable_feature = self.get_config("example.enable_advanced", False) - max_length = self.get_config("example.max_length", 100) - - # 执行具体逻辑 - if action_type == "greeting": - await self.send_text(f"你好!这是示例内容:{content}") - elif action_type == "info": - await self.send_text(f"信息:{content[:max_length]}") - else: - await self.send_text("执行了示例Action") - - # 记录动作信息 - await self.store_action_info( - action_build_into_prompt=True, - action_prompt_display=f"执行了示例动作:{action_type}", - action_done=True - ) - - return True, f"示例Action执行成功,类型:{action_type}" - - except Exception as e: - return False, f"执行失败:{str(e)}" -``` - -## 🎯 最佳实践 - -### 1. Action设计原则 - -- **单一职责**:每个Action只负责一个明确的功能 -- **智能激活**:合理选择激活类型,避免过度激活 -- **清晰描述**:提供准确的`action_require`帮助LLM决策 -- **错误处理**:妥善处理执行过程中的异常情况 - -### 2. 性能优化 - -- **激活控制**:使用合适的激活类型减少不必要的LLM调用 -- **并行执行**:谨慎设置`parallel_action`,避免冲突 -- **资源管理**:及时释放占用的资源 - -### 3. 调试技巧 - -- **日志记录**:在关键位置添加日志 -- **参数验证**:检查`action_data`的有效性 -- **配置测试**:测试不同配置下的行为 +具体参数与用法参见`BaseAction`基类的定义。 \ No newline at end of file diff --git a/src/plugin_system/base/base_action.py b/src/plugin_system/base/base_action.py index 7b9cef04..c108c5d8 100644 --- a/src/plugin_system/base/base_action.py +++ b/src/plugin_system/base/base_action.py @@ -49,12 +49,10 @@ class BaseAction(ABC): reasoning: 执行该动作的理由 cycle_timers: 计时器字典 thinking_id: 思考ID - expressor: 表达器对象 - replyer: 回复器对象 chat_stream: 聊天流对象 log_prefix: 日志前缀 - shutting_down: 是否正在关闭 plugin_config: 插件配置字典 + action_message: 消息数据 **kwargs: 其他参数 """ if plugin_config is None: @@ -414,23 +412,11 @@ class BaseAction(ABC): """ return await self.execute() - # def get_action_context(self, key: str, default=None): - # """获取action上下文信息 - - # Args: - # key: 上下文键名 - # default: 默认值 - - # Returns: - # Any: 上下文值或默认值 - # """ - # return self.api.get_action_context(key, default) - def get_config(self, key: str, default=None): - """获取插件配置值,支持嵌套键访问 + """获取插件配置值,使用嵌套键访问 Args: - key: 配置键名,支持嵌套访问如 "section.subsection.key" + key: 配置键名,使用嵌套访问如 "section.subsection.key" default: 默认值 Returns: From d4fe32b904c61d86f10749c71bb29914b9d9c4ba Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Fri, 25 Jul 2025 00:08:16 +0800 Subject: [PATCH 07/26] =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=B7=B2=E7=BB=8F?= =?UTF-8?q?=E5=BC=83=E7=94=A8=E7=9A=84=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/take_picture_plugin/_manifest.json | 50 -- .../take_picture_plugin/plugin(deprecated).py | 517 ------------------ 2 files changed, 567 deletions(-) delete mode 100644 plugins/take_picture_plugin/_manifest.json delete mode 100644 plugins/take_picture_plugin/plugin(deprecated).py diff --git a/plugins/take_picture_plugin/_manifest.json b/plugins/take_picture_plugin/_manifest.json deleted file mode 100644 index 0488d1de..00000000 --- a/plugins/take_picture_plugin/_manifest.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "manifest_version": 1, - "name": "AI拍照插件 (Take Picture Plugin)", - "version": "1.0.0", - "description": "基于AI图像生成的拍照插件,可以生成逼真的自拍照片,支持照片存储和展示功能。", - "author": { - "name": "SengokuCola", - "url": "https://github.com/SengokuCola" - }, - "license": "GPL-v3.0-or-later", - - "host_application": { - "min_version": "0.9.0" - }, - "homepage_url": "https://github.com/MaiM-with-u/maibot", - "repository_url": "https://github.com/MaiM-with-u/maibot", - "keywords": ["camera", "photo", "selfie", "ai", "image", "generation"], - "categories": ["AI Tools", "Image Processing", "Entertainment"], - - "default_locale": "zh-CN", - "locales_path": "_locales", - - "plugin_info": { - "is_built_in": false, - "plugin_type": "image_generator", - "api_dependencies": ["volcengine"], - "components": [ - { - "type": "action", - "name": "take_picture", - "description": "生成一张用手机拍摄的照片,比如自拍或者近照", - "activation_modes": ["keyword"], - "keywords": ["拍张照", "自拍", "发张照片", "看看你", "你的照片"] - }, - { - "type": "command", - "name": "show_recent_pictures", - "description": "展示最近生成的5张照片", - "pattern": "/show_pics" - } - ], - "features": [ - "AI驱动的自拍照生成", - "个性化照片风格", - "照片历史记录", - "缓存机制优化", - "火山引擎API集成" - ] - } -} \ No newline at end of file diff --git a/plugins/take_picture_plugin/plugin(deprecated).py b/plugins/take_picture_plugin/plugin(deprecated).py deleted file mode 100644 index 24e86fec..00000000 --- a/plugins/take_picture_plugin/plugin(deprecated).py +++ /dev/null @@ -1,517 +0,0 @@ -""" -拍照插件 - -功能特性: -- Action: 生成一张自拍照,prompt由人设和模板生成 -- Command: 展示最近生成的照片 - -#此插件并不完善 -#此插件并不完善 - -#此插件并不完善 - -#此插件并不完善 - -#此插件并不完善 - -#此插件并不完善 - -#此插件并不完善 - - - -包含组件: -- 拍照Action - 生成自拍照 -- 展示照片Command - 展示最近生成的照片 -""" - -from typing import List, Tuple, Type, Optional -import random -import datetime -import json -import os -import asyncio -import urllib.request -import urllib.error -import base64 -import traceback - -from src.plugin_system.base.base_plugin import BasePlugin -from src.plugin_system.base.base_action import BaseAction -from src.plugin_system.base.base_command import BaseCommand -from src.plugin_system.base.component_types import ComponentInfo, ActionActivationType, ChatMode -from src.plugin_system.base.config_types import ConfigField -from src.plugin_system import register_plugin -from src.common.logger import get_logger - -logger = get_logger("take_picture_plugin") - -# 定义数据目录常量 -DATA_DIR = os.path.join("data", "take_picture_data") -# 确保数据目录存在 -os.makedirs(DATA_DIR, exist_ok=True) -# 创建全局锁 -file_lock = asyncio.Lock() - - -class TakePictureAction(BaseAction): - """生成一张自拍照""" - - focus_activation_type = ActionActivationType.KEYWORD - normal_activation_type = ActionActivationType.KEYWORD - mode_enable = ChatMode.ALL - parallel_action = False - - action_name = "take_picture" - action_description = "生成一张用手机拍摄,比如自拍或者近照" - activation_keywords = ["拍张照", "自拍", "发张照片", "看看你", "你的照片"] - keyword_case_sensitive = False - - action_parameters = {} - - action_require = ["当用户想看你的照片时使用", "当用户让你发自拍时使用当想随手拍眼前的场景时使用"] - - associated_types = ["text", "image"] - - # 内置的Prompt模板,如果配置文件中没有定义,将使用这些模板 - DEFAULT_PROMPT_TEMPLATES = [ - "极其频繁无奇的iPhone自拍照,没有明确的主体或构图感,就是随手一拍的快照照片略带运动模糊,阳光或室内打光不均匀导致的轻微曝光过度,整体呈现出一种刻意的平庸感,就像是从口袋里拿手机时不小心拍到的一张自拍。主角是{name},{personality}" - ] - - # 简单的请求缓存,避免短时间内重复请求 - _request_cache = {} - - async def execute(self) -> Tuple[bool, Optional[str]]: - logger.info(f"{self.log_prefix} 执行拍照动作") - - try: - # 配置验证 - http_base_url = self.api.get_config("api.base_url") - http_api_key = self.api.get_config("api.volcano_generate_api_key") - - if not (http_base_url and http_api_key): - error_msg = "抱歉,照片生成功能所需的API配置(如API地址或密钥)不完整,无法提供服务。" - await self.send_text(error_msg) - logger.error(f"{self.log_prefix} HTTP调用配置缺失: base_url 或 volcano_generate_api_key.") - return False, "API配置不完整" - - # API密钥验证 - if http_api_key == "YOUR_DOUBAO_API_KEY_HERE": - error_msg = "照片生成功能尚未配置,请设置正确的API密钥。" - await self.send_text(error_msg) - logger.error(f"{self.log_prefix} API密钥未配置") - return False, "API密钥未配置" - - # 获取全局配置信息 - bot_nickname = self.api.get_global_config("bot.nickname", "麦麦") - bot_personality = self.api.get_global_config("personality.personality_core", "") - - personality_side = self.api.get_global_config("personality.personality_side", []) - if personality_side: - bot_personality += random.choice(personality_side) - - # 准备模板变量 - template_vars = {"name": bot_nickname, "personality": bot_personality} - - logger.info(f"{self.log_prefix} 使用的全局配置: name={bot_nickname}, personality={bot_personality}") - - # 尝试从配置文件获取模板,如果没有则使用默认模板 - templates = self.api.get_config("picture.prompt_templates", self.DEFAULT_PROMPT_TEMPLATES) - if not templates: - logger.warning(f"{self.log_prefix} 未找到有效的提示词模板,使用默认模板") - templates = self.DEFAULT_PROMPT_TEMPLATES - - prompt_template = random.choice(templates) - - # 填充模板 - final_prompt = prompt_template.format(**template_vars) - - logger.info(f"{self.log_prefix} 生成的最终Prompt: {final_prompt}") - - # 从配置获取参数 - model = self.api.get_config("picture.default_model", "doubao-seedream-3-0-t2i-250415") - size = self.api.get_config("picture.default_size", "1024x1024") - watermark = self.api.get_config("picture.default_watermark", True) - guidance_scale = self.api.get_config("picture.default_guidance_scale", 2.5) - seed = self.api.get_config("picture.default_seed", 42) - - # 检查缓存 - enable_cache = self.api.get_config("storage.enable_cache", True) - if enable_cache: - cache_key = self._get_cache_key(final_prompt, model, size) - if cache_key in self._request_cache: - cached_result = self._request_cache[cache_key] - logger.info(f"{self.log_prefix} 使用缓存的图片结果") - await self.send_text("我之前拍过类似的照片,用之前的结果~") - - # 直接发送缓存的结果 - send_success = await self._send_image(cached_result) - if send_success: - await self.send_text("这是我的照片,好看吗?") - return True, "照片已发送(缓存)" - else: - # 缓存失败,清除这个缓存项并继续正常流程 - del self._request_cache[cache_key] - - await self.send_text("正在为你拍照,请稍候...") - - try: - seed = random.randint(1, 1000000) - success, result = await asyncio.to_thread( - self._make_http_image_request, - prompt=final_prompt, - model=model, - size=size, - seed=seed, - guidance_scale=guidance_scale, - watermark=watermark, - ) - except Exception as e: - logger.error(f"{self.log_prefix} (HTTP) 异步请求执行失败: {e!r}", exc_info=True) - traceback.print_exc() - success = False - result = f"照片生成服务遇到意外问题: {str(e)[:100]}" - - if success: - image_url = result - logger.info(f"{self.log_prefix} 图片URL获取成功: {image_url[:70]}... 下载并编码.") - - try: - encode_success, encode_result = await asyncio.to_thread(self._download_and_encode_base64, image_url) - except Exception as e: - logger.error(f"{self.log_prefix} (B64) 异步下载/编码失败: {e!r}", exc_info=True) - traceback.print_exc() - encode_success = False - encode_result = f"图片下载或编码时发生内部错误: {str(e)[:100]}" - - if encode_success: - base64_image_string = encode_result - # 更新缓存 - if enable_cache: - self._update_cache(final_prompt, model, size, base64_image_string) - - # 发送图片 - send_success = await self._send_image(base64_image_string) - if send_success: - # 存储到文件 - await self._store_picture_info(final_prompt, image_url) - logger.info(f"{self.log_prefix} 成功生成并存储照片: {image_url}") - await self.send_text("当当当当~这是我刚拍的照片,好看吗?") - return True, f"成功生成照片: {image_url}" - else: - await self.send_text("照片生成了,但发送失败了,可能是格式问题...") - return False, "照片发送失败" - else: - await self.send_text(f"照片下载失败: {encode_result}") - return False, encode_result - else: - await self.send_text(f"哎呀,拍照失败了: {result}") - return False, result - - except Exception as e: - logger.error(f"{self.log_prefix} 执行拍照动作失败: {e}", exc_info=True) - traceback.print_exc() - await self.send_text("呜呜,拍照的时候出了一点小问题...") - return False, str(e) - - async def _store_picture_info(self, prompt: str, image_url: str): - """将照片信息存入日志文件""" - log_file = self.api.get_config("storage.log_file", "picture_log.json") - log_path = os.path.join(DATA_DIR, log_file) - max_photos = self.api.get_config("storage.max_photos", 50) - - async with file_lock: - try: - if os.path.exists(log_path): - with open(log_path, "r", encoding="utf-8") as f: - log_data = json.load(f) - else: - log_data = [] - except (json.JSONDecodeError, FileNotFoundError): - log_data = [] - - # 添加新照片 - log_data.append( - {"prompt": prompt, "image_url": image_url, "timestamp": datetime.datetime.now().isoformat()} - ) - - # 如果超过最大数量,删除最旧的 - if len(log_data) > max_photos: - log_data = sorted(log_data, key=lambda x: x.get("timestamp", ""), reverse=True)[:max_photos] - - try: - with open(log_path, "w", encoding="utf-8") as f: - json.dump(log_data, f, ensure_ascii=False, indent=4) - except Exception as e: - logger.error(f"{self.log_prefix} 写入照片日志文件失败: {e}", exc_info=True) - - def _make_http_image_request( - self, prompt: str, model: str, size: str, seed: int, guidance_scale: float, watermark: bool - ) -> Tuple[bool, str]: - """发送HTTP请求到火山引擎豆包API生成图片""" - try: - base_url = self.api.get_config("api.base_url") - api_key = self.api.get_config("api.volcano_generate_api_key") - - # 构建请求URL和头部 - endpoint = f"{base_url.rstrip('/')}/images/generations" - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {api_key}", - } - - # 构建请求体 - request_body = { - "model": model, - "prompt": prompt, - "response_format": "url", - "size": size, - "seed": seed, - "guidance_scale": guidance_scale, - "watermark": watermark, - "api-key": api_key, - } - - # 创建请求对象 - req = urllib.request.Request( - endpoint, - data=json.dumps(request_body).encode("utf-8"), - headers=headers, - method="POST", - ) - - # 发送请求并获取响应 - with urllib.request.urlopen(req, timeout=60) as response: - response_data = json.loads(response.read().decode("utf-8")) - - # 解析响应 - image_url = None - if ( - isinstance(response_data.get("data"), list) - and response_data["data"] - and isinstance(response_data["data"][0], dict) - ): - image_url = response_data["data"][0].get("url") - elif response_data.get("url"): - image_url = response_data.get("url") - - if image_url: - return True, image_url - else: - error_msg = response_data.get("error", {}).get("message", "未知错误") - logger.error(f"API返回错误: {error_msg}") - return False, f"API错误: {error_msg}" - - except urllib.error.HTTPError as e: - error_body = e.read().decode("utf-8") - logger.error(f"HTTP错误 {e.code}: {error_body}") - return False, f"HTTP错误 {e.code}: {error_body[:100]}..." - except Exception as e: - logger.error(f"请求异常: {e}", exc_info=True) - return False, f"请求异常: {str(e)}" - - def _download_and_encode_base64(self, image_url: str) -> Tuple[bool, str]: - """下载图片并转换为Base64编码""" - try: - with urllib.request.urlopen(image_url) as response: - image_data = response.read() - - base64_encoded = base64.b64encode(image_data).decode("utf-8") - return True, base64_encoded - except Exception as e: - logger.error(f"图片下载编码失败: {e}", exc_info=True) - return False, str(e) - - async def _send_image(self, base64_image: str) -> bool: - """发送图片""" - try: - # 使用聊天流信息确定发送目标 - chat_stream = self.api.get_service("chat_stream") - if not chat_stream: - logger.error(f"{self.log_prefix} 没有可用的聊天流发送图片") - return False - - if chat_stream.group_info: - # 群聊 - return await self.api.send_message_to_target( - message_type="image", - content=base64_image, - platform=chat_stream.platform, - target_id=str(chat_stream.group_info.group_id), - is_group=True, - display_message="发送生成的照片", - ) - else: - # 私聊 - return await self.api.send_message_to_target( - message_type="image", - content=base64_image, - platform=chat_stream.platform, - target_id=str(chat_stream.user_info.user_id), - is_group=False, - display_message="发送生成的照片", - ) - except Exception as e: - logger.error(f"{self.log_prefix} 发送图片时出错: {e}") - return False - - @classmethod - def _get_cache_key(cls, description: str, model: str, size: str) -> str: - """生成缓存键""" - return f"{description}|{model}|{size}" - - def _update_cache(self, description: str, model: str, size: str, base64_image: str): - """更新缓存""" - max_cache_size = self.api.get_config("storage.max_cache_size", 10) - cache_key = self._get_cache_key(description, model, size) - - # 添加到缓存 - self._request_cache[cache_key] = base64_image - - # 如果缓存超过最大大小,删除最旧的项 - if len(self._request_cache) > max_cache_size: - oldest_key = next(iter(self._request_cache)) - del self._request_cache[oldest_key] - - -class ShowRecentPicturesCommand(BaseCommand): - """展示最近生成的照片""" - - command_name = "show_recent_pictures" - command_description = "展示最近生成的5张照片" - command_pattern = r"^/show_pics$" - command_help = "用法: /show_pics" - command_examples = ["/show_pics"] - intercept_message = True - - async def execute(self) -> Tuple[bool, Optional[str]]: - logger.info(f"{self.log_prefix} 执行展示最近照片命令") - log_file = self.api.get_config("storage.log_file", "picture_log.json") - log_path = os.path.join(DATA_DIR, log_file) - - async with file_lock: - try: - if not os.path.exists(log_path): - await self.send_text("最近还没有拍过照片哦,快让我自拍一张吧!") - return True, "没有照片日志文件" - - with open(log_path, "r", encoding="utf-8") as f: - log_data = json.load(f) - - if not log_data: - await self.send_text("最近还没有拍过照片哦,快让我自拍一张吧!") - return True, "没有照片" - - # 获取最新的5张照片 - recent_pics = sorted(log_data, key=lambda x: x["timestamp"], reverse=True)[:5] - - # 先发送文本消息 - await self.send_text("这是我最近拍的几张照片~") - - # 逐个发送图片 - for pic in recent_pics: - # 尝试获取图片URL - image_url = pic.get("image_url") - if image_url: - try: - # 下载图片并转换为Base64 - with urllib.request.urlopen(image_url) as response: - image_data = response.read() - base64_encoded = base64.b64encode(image_data).decode("utf-8") - - # 发送图片 - await self.send_type( - message_type="image", content=base64_encoded, display_message="发送最近的照片" - ) - except Exception as e: - logger.error(f"{self.log_prefix} 下载或发送照片失败: {e}", exc_info=True) - - return True, "成功展示最近的照片" - - except json.JSONDecodeError: - await self.send_text("照片记录文件好像损坏了...") - return False, "JSON解码错误" - except Exception as e: - logger.error(f"{self.log_prefix} 展示照片失败: {e}", exc_info=True) - await self.send_text("哎呀,查找照片的时候出错了。") - return False, str(e) - - -@register_plugin -class TakePicturePlugin(BasePlugin): - """拍照插件""" - - plugin_name = "take_picture_plugin" # 内部标识符 - enable_plugin = False - dependencies = [] # 插件依赖列表 - python_dependencies = [] # Python包依赖列表 - config_file_name = "config.toml" - - # 配置节描述 - config_section_descriptions = { - "plugin": "插件基本信息配置", - "api": "API相关配置,包含火山引擎API的访问信息", - "components": "组件启用控制", - "picture": "拍照功能核心配置", - "storage": "照片存储相关配置", - } - - # 配置Schema定义 - config_schema = { - "plugin": { - "enabled": ConfigField(type=bool, default=False, description="是否启用插件"), - }, - "api": { - "base_url": ConfigField( - type=str, - default="https://ark.cn-beijing.volces.com/api/v3", - description="API基础URL", - example="https://api.example.com/v1", - ), - "volcano_generate_api_key": ConfigField( - type=str, default="YOUR_DOUBAO_API_KEY_HERE", description="火山引擎豆包API密钥", required=True - ), - }, - "components": { - "enable_take_picture_action": ConfigField(type=bool, default=True, description="是否启用拍照Action"), - "enable_show_pics_command": ConfigField(type=bool, default=True, description="是否启用展示照片Command"), - }, - "picture": { - "default_model": ConfigField( - type=str, - default="doubao-seedream-3-0-t2i-250415", - description="默认使用的文生图模型", - choices=["doubao-seedream-3-0-t2i-250415", "doubao-seedream-2-0-t2i"], - ), - "default_size": ConfigField( - type=str, - default="1024x1024", - description="默认图片尺寸", - example="1024x1024", - choices=["1024x1024", "1024x1280", "1280x1024", "1024x1536", "1536x1024"], - ), - "default_watermark": ConfigField(type=bool, default=True, description="是否默认添加水印"), - "default_guidance_scale": ConfigField( - type=float, default=2.5, description="模型指导强度,影响图片与提示的关联性", example="2.0" - ), - "default_seed": ConfigField(type=int, default=42, description="随机种子,用于复现图片"), - "prompt_templates": ConfigField( - type=list, default=TakePictureAction.DEFAULT_PROMPT_TEMPLATES, description="用于生成自拍照的prompt模板" - ), - }, - "storage": { - "max_photos": ConfigField(type=int, default=50, description="最大保存的照片数量"), - "log_file": ConfigField(type=str, default="picture_log.json", description="照片日志文件名"), - "enable_cache": ConfigField(type=bool, default=True, description="是否启用请求缓存"), - "max_cache_size": ConfigField(type=int, default=10, description="最大缓存数量"), - }, - } - - def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: - """返回插件包含的组件列表""" - components = [] - if self.get_config("components.enable_take_picture_action", True): - components.append((TakePictureAction.get_action_info(), TakePictureAction)) - if self.get_config("components.enable_show_pics_command", True): - components.append((ShowRecentPicturesCommand.get_command_info(), ShowRecentPicturesCommand)) - return components From 87e81d4330f0e3eeaf0f72f1e3587e919d112a3c Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Jul 2025 00:31:15 +0800 Subject: [PATCH 08/26] Update heartFC_chat.py --- src/chat/chat_loop/heartFC_chat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index f7fb974c..41101b2d 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -236,10 +236,10 @@ class HeartFChatting: if if_think: factor = max(global_config.chat.focus_value, 0.1) self.energy_value *= 1.1 / factor - logger.info(f"{self.log_prefix} 麦麦进行了思考,能量值按倍数增加,当前能量值:{self.energy_value:.1f}") + logger.info(f"{self.log_prefix} 进行了思考,能量值按倍数增加,当前能量值:{self.energy_value:.1f}") else: self.energy_value += 0.1 / global_config.chat.focus_value - logger.info(f"{self.log_prefix} 麦麦没有进行思考,能量值线性增加,当前能量值:{self.energy_value:.1f}") + logger.debug(f"{self.log_prefix} 没有进行思考,能量值线性增加,当前能量值:{self.energy_value:.1f}") logger.debug(f"{self.log_prefix} 当前能量值:{self.energy_value:.1f}") return True From 3495926f55fb72aa56f39fbee059668f1a3c8c92 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Jul 2025 02:01:28 +0800 Subject: [PATCH 09/26] Update utils_model.py --- src/llm_models/utils_model.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/llm_models/utils_model.py b/src/llm_models/utils_model.py index 8a121588..c994cd17 100644 --- a/src/llm_models/utils_model.py +++ b/src/llm_models/utils_model.py @@ -10,6 +10,7 @@ import base64 from PIL import Image import io import os +import copy # 添加copy模块用于深拷贝 from src.common.database.database import db # 确保 db 被导入用于 create_tables from src.common.database.database_model import LLMUsage # 导入 LLMUsage 模型 from src.config.config import global_config @@ -69,23 +70,28 @@ error_code_mapping = { async def _safely_record(request_content: Dict[str, Any], payload: Dict[str, Any]): + """安全地记录请求体,用于调试日志,不会修改原始payload对象""" + # 创建payload的深拷贝,避免修改原始对象 + safe_payload = copy.deepcopy(payload) + image_base64: str = request_content.get("image_base64") image_format: str = request_content.get("image_format") if ( image_base64 - and payload - and isinstance(payload, dict) - and "messages" in payload - and len(payload["messages"]) > 0 + and safe_payload + and isinstance(safe_payload, dict) + and "messages" in safe_payload + and len(safe_payload["messages"]) > 0 ): - if isinstance(payload["messages"][0], dict) and "content" in payload["messages"][0]: - content = payload["messages"][0]["content"] + if isinstance(safe_payload["messages"][0], dict) and "content" in safe_payload["messages"][0]: + content = safe_payload["messages"][0]["content"] if isinstance(content, list) and len(content) > 1 and "image_url" in content[1]: - payload["messages"][0]["content"][1]["image_url"]["url"] = ( + # 只修改拷贝的对象,用于安全的日志记录 + safe_payload["messages"][0]["content"][1]["image_url"]["url"] = ( f"data:image/{image_format.lower() if image_format else 'jpeg'};base64," f"{image_base64[:10]}...{image_base64[-10:]}" ) - return payload + return safe_payload class LLMRequest: From bbb112d8038f1afd2139c7917648c32d39e669c3 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Fri, 25 Jul 2025 11:01:14 +0800 Subject: [PATCH 10/26] =?UTF-8?q?=E5=8A=A8=E6=80=81=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E5=90=8E=E7=BB=AD=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changes.md | 2 + plugins/hello_world_plugin/plugin.py | 5 +-- src/chat/message_receive/bot.py | 7 +--- src/plugin_system/base/base_command.py | 8 +--- src/plugin_system/base/component_types.py | 1 - .../built_in/plugin_management/plugin.py | 39 +++++++++---------- 6 files changed, 27 insertions(+), 35 deletions(-) diff --git a/changes.md b/changes.md index 70b9dfbf..7d4f2ae8 100644 --- a/changes.md +++ b/changes.md @@ -21,6 +21,8 @@ - `database_api.py`中的`db_query`方法调整了参数顺序以增强参数限制的同时,保证了typing正确;`db_get`方法增加了`single_result`参数,与`db_query`保持一致。 5. 增加了`logging_api`,可以用`get_logger`来获取日志记录器。 6. 增加了插件和组件管理的API。 +7. `BaseCommand`的`execute`方法现在返回一个三元组,包含是否执行成功、可选的回复消息和是否拦截消息。 + - 这意味着你终于可以动态控制是否继续后续消息的处理了。 # 插件系统修改 1. 现在所有的匹配模式不再是关键字了,而是枚举类。**(可能有遗漏)** diff --git a/plugins/hello_world_plugin/plugin.py b/plugins/hello_world_plugin/plugin.py index 55b9df82..11ff22bd 100644 --- a/plugins/hello_world_plugin/plugin.py +++ b/plugins/hello_world_plugin/plugin.py @@ -77,9 +77,8 @@ class TimeCommand(BaseCommand): command_pattern = r"^/time$" # 精确匹配 "/time" 命令 command_help = "查询当前时间" command_examples = ["/time"] - intercept_message = True # 拦截消息,不让其他组件处理 - async def execute(self) -> Tuple[bool, str]: + async def execute(self) -> Tuple[bool, str, bool]: """执行时间查询""" import datetime @@ -92,7 +91,7 @@ class TimeCommand(BaseCommand): message = f"⏰ 当前时间:{time_str}" await self.send_text(message) - return True, f"显示了当前时间: {time_str}" + return True, f"显示了当前时间: {time_str}", True class PrintMessage(BaseEventHandler): diff --git a/src/chat/message_receive/bot.py b/src/chat/message_receive/bot.py index cade4f14..a4228b89 100644 --- a/src/chat/message_receive/bot.py +++ b/src/chat/message_receive/bot.py @@ -92,7 +92,6 @@ class ChatBot: command_result = component_registry.find_command_by_text(text) if command_result: command_class, matched_groups, command_info = command_result - intercept_message = command_info.intercept_message plugin_name = command_info.plugin_name command_name = command_info.name if ( @@ -115,7 +114,7 @@ class ChatBot: try: # 执行命令 - success, response = await command_instance.execute() + success, response, intercept_message = await command_instance.execute() # 记录命令执行结果 if success: @@ -128,8 +127,6 @@ class ChatBot: except Exception as e: logger.error(f"执行命令时出错: {command_class.__name__} - {e}") - import traceback - logger.error(traceback.format_exc()) try: @@ -138,7 +135,7 @@ class ChatBot: logger.error(f"发送错误消息失败: {send_error}") # 命令出错时,根据命令的拦截设置决定是否继续处理消息 - return True, str(e), not intercept_message + return True, str(e), False # 出错时继续处理消息 # 没有找到命令,继续处理消息 return False, None, True diff --git a/src/plugin_system/base/base_command.py b/src/plugin_system/base/base_command.py index 7909980c..b79f6884 100644 --- a/src/plugin_system/base/base_command.py +++ b/src/plugin_system/base/base_command.py @@ -17,7 +17,6 @@ class BaseCommand(ABC): - command_pattern: 命令匹配的正则表达式 - command_help: 命令帮助信息 - command_examples: 命令使用示例列表 - - intercept_message: 是否拦截消息处理(默认True拦截,False继续传递) """ command_name: str = "" @@ -30,8 +29,6 @@ class BaseCommand(ABC): command_help: str = "" """命令帮助信息""" command_examples: List[str] = [] - intercept_message: bool = True - """是否拦截信息,默认拦截,不进行后续处理""" def __init__(self, message: MessageRecv, plugin_config: Optional[dict] = None): """初始化Command组件 @@ -57,11 +54,11 @@ class BaseCommand(ABC): self.matched_groups = groups @abstractmethod - async def execute(self) -> Tuple[bool, Optional[str]]: + async def execute(self) -> Tuple[bool, Optional[str], bool]: """执行Command的抽象方法,子类必须实现 Returns: - Tuple[bool, Optional[str]]: (是否执行成功, 可选的回复消息) + Tuple[bool, Optional[str], bool]: (是否执行成功, 可选的回复消息, 是否拦截消息 不进行 后续处理) """ pass @@ -233,5 +230,4 @@ class BaseCommand(ABC): command_pattern=cls.command_pattern, command_help=cls.command_help, command_examples=cls.command_examples.copy() if cls.command_examples else [], - intercept_message=cls.intercept_message, ) diff --git a/src/plugin_system/base/component_types.py b/src/plugin_system/base/component_types.py index 774daa59..74b01ddd 100644 --- a/src/plugin_system/base/component_types.py +++ b/src/plugin_system/base/component_types.py @@ -142,7 +142,6 @@ class CommandInfo(ComponentInfo): command_pattern: str = "" # 命令匹配模式(正则表达式) command_help: str = "" # 命令帮助信息 command_examples: List[str] = field(default_factory=list) # 命令使用示例 - intercept_message: bool = True # 是否拦截消息处理(默认拦截) def __post_init__(self): super().__post_init__() diff --git a/src/plugins/built_in/plugin_management/plugin.py b/src/plugins/built_in/plugin_management/plugin.py index 67e2a5f6..cbdf567a 100644 --- a/src/plugins/built_in/plugin_management/plugin.py +++ b/src/plugins/built_in/plugin_management/plugin.py @@ -18,9 +18,8 @@ class ManagementCommand(BaseCommand): command_name: str = "management" description: str = "管理命令" command_pattern: str = r"(?P^/pm(\s[a-zA-Z0-9_]+)*\s*$)" - intercept_message: bool = True - async def execute(self) -> Tuple[bool, str]: + async def execute(self) -> Tuple[bool, str, bool]: # sourcery skip: merge-duplicate-blocks if ( not self.message @@ -29,11 +28,11 @@ class ManagementCommand(BaseCommand): or str(self.message.message_info.user_info.user_id) not in self.get_config("plugin.permission", []) # type: ignore ): await self.send_text("你没有权限使用插件管理命令") - return False, "没有权限" + return False, "没有权限", True command_list = self.matched_groups["manage_command"].strip().split(" ") if len(command_list) == 1: await self.show_help("all") - return True, "帮助已发送" + return True, "帮助已发送", True if len(command_list) == 2: match command_list[1]: case "plugin": @@ -44,7 +43,7 @@ class ManagementCommand(BaseCommand): await self.show_help("all") case _: await self.send_text("插件管理命令不合法") - return False, "命令不合法" + return False, "命令不合法", True if len(command_list) == 3: if command_list[1] == "plugin": match command_list[2]: @@ -58,7 +57,7 @@ class ManagementCommand(BaseCommand): await self._rescan_plugin_dirs() case _: await self.send_text("插件管理命令不合法") - return False, "命令不合法" + return False, "命令不合法", True elif command_list[1] == "component": if command_list[2] == "list": await self._list_all_registered_components() @@ -66,10 +65,10 @@ class ManagementCommand(BaseCommand): await self.show_help("component") else: await self.send_text("插件管理命令不合法") - return False, "命令不合法" + return False, "命令不合法", True else: await self.send_text("插件管理命令不合法") - return False, "命令不合法" + return False, "命令不合法", True if len(command_list) == 4: if command_list[1] == "plugin": match command_list[2]: @@ -83,28 +82,28 @@ class ManagementCommand(BaseCommand): await self._add_dir(command_list[3]) case _: await self.send_text("插件管理命令不合法") - return False, "命令不合法" + return False, "命令不合法", True elif command_list[1] == "component": if command_list[2] != "list": await self.send_text("插件管理命令不合法") - return False, "命令不合法" + return False, "命令不合法", True if command_list[3] == "enabled": await self._list_enabled_components() elif command_list[3] == "disabled": await self._list_disabled_components() else: await self.send_text("插件管理命令不合法") - return False, "命令不合法" + return False, "命令不合法", True else: await self.send_text("插件管理命令不合法") - return False, "命令不合法" + return False, "命令不合法", True if len(command_list) == 5: if command_list[1] != "component": await self.send_text("插件管理命令不合法") - return False, "命令不合法" + return False, "命令不合法", True if command_list[2] != "list": await self.send_text("插件管理命令不合法") - return False, "命令不合法" + return False, "命令不合法", True if command_list[3] == "enabled": await self._list_enabled_components(target_type=command_list[4]) elif command_list[3] == "disabled": @@ -113,11 +112,11 @@ class ManagementCommand(BaseCommand): await self._list_registered_components_by_type(command_list[4]) else: await self.send_text("插件管理命令不合法") - return False, "命令不合法" + return False, "命令不合法", True if len(command_list) == 6: if command_list[1] != "component": await self.send_text("插件管理命令不合法") - return False, "命令不合法" + return False, "命令不合法", True if command_list[2] == "enable": if command_list[3] == "global": await self._globally_enable_component(command_list[4], command_list[5]) @@ -125,7 +124,7 @@ class ManagementCommand(BaseCommand): await self._locally_enable_component(command_list[4], command_list[5]) else: await self.send_text("插件管理命令不合法") - return False, "命令不合法" + return False, "命令不合法", True elif command_list[2] == "disable": if command_list[3] == "global": await self._globally_disable_component(command_list[4], command_list[5]) @@ -133,12 +132,12 @@ class ManagementCommand(BaseCommand): await self._locally_disable_component(command_list[4], command_list[5]) else: await self.send_text("插件管理命令不合法") - return False, "命令不合法" + return False, "命令不合法", True else: await self.send_text("插件管理命令不合法") - return False, "命令不合法" + return False, "命令不合法", True - return True, "命令执行完成" + return True, "命令执行完成", True async def show_help(self, target: str): help_msg = "" From 229d45083d04ae43b4c2c559ed4fa46e577f2daf Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Fri, 25 Jul 2025 13:39:27 +0800 Subject: [PATCH 11/26] =?UTF-8?q?command=E7=AE=80=E5=8C=96=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8Dunregister=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/hello_world_plugin/plugin.py | 6 ++---- src/plugin_system/base/base_command.py | 7 +------ src/plugin_system/base/component_types.py | 4 ---- src/plugin_system/core/events_manager.py | 22 +++++++++++----------- 4 files changed, 14 insertions(+), 25 deletions(-) diff --git a/plugins/hello_world_plugin/plugin.py b/plugins/hello_world_plugin/plugin.py index 11ff22bd..8ede9616 100644 --- a/plugins/hello_world_plugin/plugin.py +++ b/plugins/hello_world_plugin/plugin.py @@ -20,6 +20,7 @@ class HelloAction(BaseAction): # === 基本信息(必须填写)=== action_name = "hello_greeting" action_description = "向用户发送问候消息" + activation_type = ActionActivationType.ALWAYS # 始终激活 # === 功能描述(必须填写)=== action_parameters = {"greeting_message": "要发送的问候消息"} @@ -44,8 +45,7 @@ class ByeAction(BaseAction): action_description = "向用户发送告别消息" # 使用关键词激活 - focus_activation_type = ActionActivationType.KEYWORD - normal_activation_type = ActionActivationType.KEYWORD + activation_type = ActionActivationType.KEYWORD # 关键词设置 activation_keywords = ["再见", "bye", "88", "拜拜"] @@ -75,8 +75,6 @@ class TimeCommand(BaseCommand): # === 命令设置(必须填写)=== command_pattern = r"^/time$" # 精确匹配 "/time" 命令 - command_help = "查询当前时间" - command_examples = ["/time"] async def execute(self) -> Tuple[bool, str, bool]: """执行时间查询""" diff --git a/src/plugin_system/base/base_command.py b/src/plugin_system/base/base_command.py index b79f6884..60ee99ad 100644 --- a/src/plugin_system/base/base_command.py +++ b/src/plugin_system/base/base_command.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Dict, Tuple, Optional, List +from typing import Dict, Tuple, Optional from src.common.logger import get_logger from src.plugin_system.base.component_types import CommandInfo, ComponentType from src.chat.message_receive.message import MessageRecv @@ -26,9 +26,6 @@ class BaseCommand(ABC): # 默认命令设置 command_pattern: str = r"" """命令匹配的正则表达式""" - command_help: str = "" - """命令帮助信息""" - command_examples: List[str] = [] def __init__(self, message: MessageRecv, plugin_config: Optional[dict] = None): """初始化Command组件 @@ -228,6 +225,4 @@ class BaseCommand(ABC): component_type=ComponentType.COMMAND, description=cls.command_description, command_pattern=cls.command_pattern, - command_help=cls.command_help, - command_examples=cls.command_examples.copy() if cls.command_examples else [], ) diff --git a/src/plugin_system/base/component_types.py b/src/plugin_system/base/component_types.py index 74b01ddd..eeb2a5a0 100644 --- a/src/plugin_system/base/component_types.py +++ b/src/plugin_system/base/component_types.py @@ -140,13 +140,9 @@ class CommandInfo(ComponentInfo): """命令组件信息""" command_pattern: str = "" # 命令匹配模式(正则表达式) - command_help: str = "" # 命令帮助信息 - command_examples: List[str] = field(default_factory=list) # 命令使用示例 def __post_init__(self): super().__post_init__() - if self.command_examples is None: - self.command_examples = [] self.component_type = ComponentType.COMMAND diff --git a/src/plugin_system/core/events_manager.py b/src/plugin_system/core/events_manager.py index 1f01b4ab..3c215a7f 100644 --- a/src/plugin_system/core/events_manager.py +++ b/src/plugin_system/core/events_manager.py @@ -182,17 +182,17 @@ class EventsManager: async def cancel_handler_tasks(self, handler_name: str) -> None: tasks_to_be_cancelled = self._handler_tasks.get(handler_name, []) - remaining_tasks = [task for task in tasks_to_be_cancelled if not task.done()] - for task in remaining_tasks: - task.cancel() - try: - await asyncio.wait_for(asyncio.gather(*remaining_tasks, return_exceptions=True), timeout=5) - logger.info(f"已取消事件处理器 {handler_name} 的所有任务") - except asyncio.TimeoutError: - logger.warning(f"取消事件处理器 {handler_name} 的任务超时,开始强制取消") - except Exception as e: - logger.error(f"取消事件处理器 {handler_name} 的任务时发生异常: {e}") - finally: + if remaining_tasks := [task for task in tasks_to_be_cancelled if not task.done()]: + for task in remaining_tasks: + task.cancel() + try: + await asyncio.wait_for(asyncio.gather(*remaining_tasks, return_exceptions=True), timeout=5) + logger.info(f"已取消事件处理器 {handler_name} 的所有任务") + except asyncio.TimeoutError: + logger.warning(f"取消事件处理器 {handler_name} 的任务超时,开始强制取消") + except Exception as e: + logger.error(f"取消事件处理器 {handler_name} 的任务时发生异常: {e}") + if handler_name in self._handler_tasks: del self._handler_tasks[handler_name] async def unregister_event_subscriber(self, handler_name: str) -> bool: From 5251905744461c2213938fed7d5642e8645aa900 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Jul 2025 13:45:16 +0800 Subject: [PATCH 12/26] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8Dreply?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E7=9A=84=E5=BC=82=E5=B8=B8=E7=A9=BA=E8=B7=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/interest_value_analysis.py | 2 +- src/chat/planner_actions/action_modifier.py | 92 +-------------------- src/chat/planner_actions/planner.py | 29 ++++--- src/person_info/relationship_builder.py | 4 +- src/plugins/built_in/core_actions/emoji.py | 3 +- 5 files changed, 21 insertions(+), 109 deletions(-) diff --git a/scripts/interest_value_analysis.py b/scripts/interest_value_analysis.py index 19007f68..fba1f160 100644 --- a/scripts/interest_value_analysis.py +++ b/scripts/interest_value_analysis.py @@ -3,10 +3,10 @@ import sys import os from typing import Dict, List, Tuple, Optional from datetime import datetime -from src.common.database.database_model import Messages, ChatStreams # Add project root to Python path project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, project_root) +from src.common.database.database_model import Messages, ChatStreams #noqa diff --git a/src/chat/planner_actions/action_modifier.py b/src/chat/planner_actions/action_modifier.py index c7964edc..dce70678 100644 --- a/src/chat/planner_actions/action_modifier.py +++ b/src/chat/planner_actions/action_modifier.py @@ -438,94 +438,4 @@ class ActionModifier: return True else: logger.debug(f"{self.log_prefix}动作 {action_name} 未匹配到任何关键词: {activation_keywords}") - return False - - # async def analyze_loop_actions(self, history_loop: List[CycleDetail]) -> List[tuple[str, str]]: - # """分析最近的循环内容并决定动作的移除 - - # Returns: - # List[Tuple[str, str]]: 包含要删除的动作及原因的元组列表 - # [("action3", "some reason")] - # """ - # removals = [] - - # # 获取最近10次循环 - # recent_cycles = history_loop[-10:] if len(history_loop) > 10 else history_loop - # if not recent_cycles: - # return removals - - # reply_sequence = [] # 记录最近的动作序列 - - # for cycle in recent_cycles: - # action_result = cycle.loop_plan_info.get("action_result", {}) - # action_type = action_result.get("action_type", "unknown") - # reply_sequence.append(action_type == "reply") - - # # 计算连续回复的相关阈值 - - # max_reply_num = int(global_config.focus_chat.consecutive_replies * 3.2) - # sec_thres_reply_num = int(global_config.focus_chat.consecutive_replies * 2) - # one_thres_reply_num = int(global_config.focus_chat.consecutive_replies * 1.5) - - # # 获取最近max_reply_num次的reply状态 - # if len(reply_sequence) >= max_reply_num: - # last_max_reply_num = reply_sequence[-max_reply_num:] - # else: - # last_max_reply_num = reply_sequence[:] - - # # 详细打印阈值和序列信息,便于调试 - # logger.info( - # f"连续回复阈值: max={max_reply_num}, sec={sec_thres_reply_num}, one={one_thres_reply_num}," - # f"最近reply序列: {last_max_reply_num}" - # ) - # # print(f"consecutive_replies: {consecutive_replies}") - - # # 根据最近的reply情况决定是否移除reply动作 - # if len(last_max_reply_num) >= max_reply_num and all(last_max_reply_num): - # # 如果最近max_reply_num次都是reply,直接移除 - # reason = f"连续回复过多(最近{len(last_max_reply_num)}次全是reply,超过阈值{max_reply_num})" - # removals.append(("reply", reason)) - # # reply_count = len(last_max_reply_num) - no_reply_count - # elif len(last_max_reply_num) >= sec_thres_reply_num and all(last_max_reply_num[-sec_thres_reply_num:]): - # # 如果最近sec_thres_reply_num次都是reply,40%概率移除 - # removal_probability = 0.4 / global_config.focus_chat.consecutive_replies - # if random.random() < removal_probability: - # reason = ( - # f"连续回复较多(最近{sec_thres_reply_num}次全是reply,{removal_probability:.2f}概率移除,触发移除)" - # ) - # removals.append(("reply", reason)) - # elif len(last_max_reply_num) >= one_thres_reply_num and all(last_max_reply_num[-one_thres_reply_num:]): - # # 如果最近one_thres_reply_num次都是reply,20%概率移除 - # removal_probability = 0.2 / global_config.focus_chat.consecutive_replies - # if random.random() < removal_probability: - # reason = ( - # f"连续回复检测(最近{one_thres_reply_num}次全是reply,{removal_probability:.2f}概率移除,触发移除)" - # ) - # removals.append(("reply", reason)) - # else: - # logger.debug(f"{self.log_prefix}连续回复检测:无需移除reply动作,最近回复模式正常") - - # return removals - - # def get_available_actions_count(self, mode: str = "focus") -> int: - # """获取当前可用动作数量(排除默认的no_action)""" - # current_actions = self.action_manager.get_using_actions_for_mode(mode) - # # 排除no_action(如果存在) - # filtered_actions = {k: v for k, v in current_actions.items() if k != "no_action"} - # return len(filtered_actions) - - # def should_skip_planning_for_no_reply(self) -> bool: - # """判断是否应该跳过规划过程""" - # current_actions = self.action_manager.get_using_actions_for_mode("focus") - # # 排除no_action(如果存在) - # if len(current_actions) == 1 and "no_reply" in current_actions: - # return True - # return False - - # def should_skip_planning_for_no_action(self) -> bool: - # """判断是否应该跳过规划过程""" - # available_count = self.action_manager.get_using_actions_for_mode("normal") - # if available_count == 0: - # logger.debug(f"{self.log_prefix} 没有可用动作,跳过规划") - # return True - # return False + return False \ No newline at end of file diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index e3d1edef..a679c495 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -40,7 +40,6 @@ def init_prompt(): {moderation_prompt} 现在请你根据{by_what}选择合适的action和触发action的消息: -你刚刚选择并执行过的action是: {actions_before_now_block} {no_action_block} @@ -130,17 +129,18 @@ class ActionPlanner: logger.warning(f"{self.log_prefix}使用中的动作 {action_name} 未在已注册动作中找到") # 如果没有可用动作或只有no_reply动作,直接返回no_reply - if not current_available_actions: - action = "no_reply" if mode == ChatMode.FOCUS else "no_action" - reasoning = "没有可用的动作" - logger.info(f"{self.log_prefix}{reasoning}") - return { - "action_result": { - "action_type": action, - "action_data": action_data, - "reasoning": reasoning, - }, - }, None + # 因为现在reply是永远激活,所以不需要空跳判定 + # if not current_available_actions: + # action = "no_reply" if mode == ChatMode.FOCUS else "no_action" + # reasoning = "没有可用的动作" + # logger.info(f"{self.log_prefix}{reasoning}") + # return { + # "action_result": { + # "action_type": action, + # "action_data": action_data, + # "reasoning": reasoning, + # }, + # }, None # --- 构建提示词 (调用修改后的 PromptBuilder 方法) --- prompt, message_id_list = await self.build_planner_prompt( @@ -268,6 +268,7 @@ class ActionPlanner: 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, ) @@ -275,6 +276,8 @@ class ActionPlanner: 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() @@ -288,7 +291,7 @@ class ActionPlanner: by_what = "聊天内容" target_prompt = '\n "target_message_id":"触发action的消息id"' - no_action_block = f"""重要说明1: + no_action_block = f"""重要说明: - 'no_reply' 表示只进行不进行回复,等待合适的回复时机 - 当你刚刚发送了消息,没有人回复时,选择no_reply - 当你一次发送了太多消息,为了避免打扰聊天节奏,选择no_reply diff --git a/src/person_info/relationship_builder.py b/src/person_info/relationship_builder.py index 69b9e84d..5bf68991 100644 --- a/src/person_info/relationship_builder.py +++ b/src/person_info/relationship_builder.py @@ -419,7 +419,7 @@ class RelationshipBuilder: async def update_impression_on_segments(self, person_id: str, chat_id: str, segments: List[Dict[str, Any]]): """基于消息段更新用户印象""" original_segment_count = len(segments) - logger.info(f"开始为 {person_id} 基于 {original_segment_count} 个消息段更新印象") + logger.debug(f"开始为 {person_id} 基于 {original_segment_count} 个消息段更新印象") try: # 筛选要处理的消息段,每个消息段有10%的概率被丢弃 segments_to_process = [s for s in segments if random.random() >= 0.1] @@ -432,7 +432,7 @@ class RelationshipBuilder: dropped_count = original_segment_count - len(segments_to_process) if dropped_count > 0: - logger.info(f"为 {person_id} 随机丢弃了 {dropped_count} / {original_segment_count} 个消息段") + logger.debug(f"为 {person_id} 随机丢弃了 {dropped_count} / {original_segment_count} 个消息段") processed_messages = [] diff --git a/src/plugins/built_in/core_actions/emoji.py b/src/plugins/built_in/core_actions/emoji.py index d44183c8..4563b47f 100644 --- a/src/plugins/built_in/core_actions/emoji.py +++ b/src/plugins/built_in/core_actions/emoji.py @@ -20,8 +20,7 @@ class EmojiAction(BaseAction): """表情动作 - 发送表情包""" # 激活设置 - focus_activation_type = ActionActivationType.RANDOM - normal_activation_type = ActionActivationType.RANDOM + activation_type = ActionActivationType.RANDOM mode_enable = ChatMode.ALL parallel_action = True random_activation_probability = 0.2 # 默认值,可通过配置覆盖 From 2aec68bd3da743550d16bfe293029432c8739615 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Jul 2025 13:51:06 +0800 Subject: [PATCH 13/26] fixruff Update heartflow_message_processor.py --- src/chat/heart_flow/heartflow_message_processor.py | 2 +- src/common/logger.py | 2 +- src/llm_models/utils_model.py | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/chat/heart_flow/heartflow_message_processor.py b/src/chat/heart_flow/heartflow_message_processor.py index 3aa174bb..32a36568 100644 --- a/src/chat/heart_flow/heartflow_message_processor.py +++ b/src/chat/heart_flow/heartflow_message_processor.py @@ -124,7 +124,7 @@ class HeartFCMessageReceiver: picid_pattern = r"\[picid:([^\]]+)\]" processed_plain_text = re.sub(picid_pattern, "[图片]", message.processed_plain_text) - logger.info(f"[{mes_name}]{userinfo.user_nickname}:{processed_plain_text}") # type: ignore + logger.info(f"[{mes_name}]{userinfo.user_nickname}:{processed_plain_text}[兴趣度:{interested_rate:.2f}]") # type: ignore logger.debug(f"[{mes_name}][当前时段回复频率: {current_talk_frequency}]") diff --git a/src/common/logger.py b/src/common/logger.py index a6bfc263..78446dec 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -390,7 +390,7 @@ MODULE_COLORS = { "tts_action": "\033[38;5;58m", # 深黄色 "doubao_pic_plugin": "\033[38;5;64m", # 深绿色 # Action组件 - "no_reply_action": "\033[38;5;196m", # 亮红色,更显眼 + "no_reply_action": "\033[38;5;214m", # 亮橙色,显眼但不像警告 "reply_action": "\033[38;5;46m", # 亮绿色 "base_action": "\033[38;5;250m", # 浅灰色 # 数据库和消息 diff --git a/src/llm_models/utils_model.py b/src/llm_models/utils_model.py index c994cd17..9aca329e 100644 --- a/src/llm_models/utils_model.py +++ b/src/llm_models/utils_model.py @@ -148,7 +148,7 @@ class LLMRequest: self.max_tokens = model.get("max_tokens", global_config.model.model_max_output_length) # print(f"max_tokens: {self.max_tokens}") - logger.debug(f"🔍 [模型初始化] 模型参数设置完成:") + logger.debug("🔍 [模型初始化] 模型参数设置完成:") logger.debug(f" - model_name: {self.model_name}") logger.debug(f" - has_enable_thinking: {self.has_enable_thinking}") logger.debug(f" - enable_thinking: {self.enable_thinking}") @@ -537,7 +537,7 @@ class LLMRequest: logger.error(f"🔍 [调试信息] 模型 {self.model_name} 参数错误 (400) - 开始详细诊断") logger.error(f"🔍 [调试信息] 模型名称: {self.model_name}") logger.error(f"🔍 [调试信息] API地址: {self.base_url}") - logger.error(f"🔍 [调试信息] 模型配置参数:") + logger.error("🔍 [调试信息] 模型配置参数:") logger.error(f" - enable_thinking: {self.enable_thinking}") logger.error(f" - temp: {self.temp}") logger.error(f" - thinking_budget: {self.thinking_budget}") @@ -556,7 +556,7 @@ class LLMRequest: error_json = json.loads(error_text) logger.error(f"🔍 [调试信息] 解析后的错误JSON: {json.dumps(error_json, indent=2, ensure_ascii=False)}") except json.JSONDecodeError: - logger.error(f"🔍 [调试信息] 错误响应不是有效的JSON格式") + logger.error("🔍 [调试信息] 错误响应不是有效的JSON格式") except Exception as e: logger.error(f"🔍 [调试信息] 无法读取错误响应内容: {str(e)}") @@ -583,7 +583,7 @@ class LLMRequest: # 如果是400错误,额外输出请求体信息用于调试 if response.status == 400: - logger.error(f"🔍 [异常调试] 400错误 - 请求体调试信息:") + logger.error("🔍 [异常调试] 400错误 - 请求体调试信息:") try: safe_payload = await _safely_record(request_content, payload) logger.error(f"🔍 [异常调试] 发送的请求体: {json.dumps(safe_payload, indent=2, ensure_ascii=False)}") @@ -743,7 +743,7 @@ class LLMRequest: logger.debug(f"🔍 [参数转换] CoT模型列表: {self.MODELS_NEEDING_TRANSFORMATION}") if self.model_name.lower() in self.MODELS_NEEDING_TRANSFORMATION: - logger.debug(f"🔍 [参数转换] 检测到CoT模型,开始参数转换") + logger.debug("🔍 [参数转换] 检测到CoT模型,开始参数转换") # 删除 'temperature' 参数(如果存在),但避免删除我们在_build_payload中添加的自定义温度 if "temperature" in new_params and new_params["temperature"] == 0.7: removed_temp = new_params.pop("temperature") @@ -754,7 +754,7 @@ class LLMRequest: new_params["max_completion_tokens"] = new_params.pop("max_tokens") logger.debug(f"🔍 [参数转换] 参数重命名: max_tokens({old_value}) -> max_completion_tokens({new_params['max_completion_tokens']})") else: - logger.debug(f"🔍 [参数转换] 非CoT模型,无需参数转换") + logger.debug("🔍 [参数转换] 非CoT模型,无需参数转换") logger.debug(f"🔍 [参数转换] 转换前参数: {params}") logger.debug(f"🔍 [参数转换] 转换后参数: {new_params}") From 6a7cf71d1d6de8991a41b7a8c4c0964a7be4fe5c Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Fri, 25 Jul 2025 14:06:41 +0800 Subject: [PATCH 14/26] =?UTF-8?q?command=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plugins/action-components.md | 1 - docs/plugins/command-components.md | 543 ++++------------------------- 2 files changed, 60 insertions(+), 484 deletions(-) diff --git a/docs/plugins/action-components.md b/docs/plugins/action-components.md index 3953c79c..4c844df8 100644 --- a/docs/plugins/action-components.md +++ b/docs/plugins/action-components.md @@ -268,7 +268,6 @@ action_message为一个字典,包含的键值对如下(省略了不必要的 ## Action 内置方法说明 ```python class BaseAction: - # 配置相关 def get_config(self, key: str, default=None): """获取插件配置值,使用嵌套键访问""" diff --git a/docs/plugins/command-components.md b/docs/plugins/command-components.md index d3eb2003..77cc8acc 100644 --- a/docs/plugins/command-components.md +++ b/docs/plugins/command-components.md @@ -2,7 +2,9 @@ ## 📖 什么是Command -Command是直接响应用户明确指令的组件,与Action不同,Command是**被动触发**的,当用户输入特定格式的命令时立即执行。Command通过正则表达式匹配用户输入,提供确定性的功能服务。 +Command是直接响应用户明确指令的组件,与Action不同,Command是**被动触发**的,当用户输入特定格式的命令时立即执行。 + +Command通过正则表达式匹配用户输入,提供确定性的功能服务。 ### 🎯 Command的特点 @@ -12,501 +14,76 @@ Command是直接响应用户明确指令的组件,与Action不同,Command是 - 🛑 **拦截控制**:可以控制是否阻止消息继续处理 - 📝 **参数解析**:支持从用户输入中提取参数 -## 🆚 Action vs Command 核心区别 +--- -| 特征 | Action | Command | -| ------------------ | --------------------- | ---------------- | -| **触发方式** | 麦麦主动决策使用 | 用户主动触发 | -| **决策机制** | 两层决策(激活+使用) | 直接匹配执行 | -| **随机性** | 有随机性和智能性 | 确定性执行 | -| **用途** | 增强麦麦行为拟人化 | 提供具体功能服务 | -| **性能影响** | 需要LLM决策 | 正则匹配,性能好 | +## 🛠️ Command组件的基本结构 -## 🏗️ Command基本结构 - -### 必须属性 +首先,Command组件需要继承自`BaseCommand`类,并实现必要的方法。 ```python -from src.plugin_system import BaseCommand +class ExampleCommand(BaseCommand): + command_name = "example" # 命令名称,作为唯一标识符 + command_description = "这是一个示例命令" # 命令描述 + command_pattern = r"" # 命令匹配的正则表达式 -class MyCommand(BaseCommand): - # 正则表达式匹配模式 - command_pattern = r"^/help\s+(?P\w+)$" - - # 命令帮助说明 - command_help = "显示指定主题的帮助信息" - - # 使用示例 - command_examples = ["/help action", "/help command"] - - # 是否拦截后续处理 - intercept_message = True - - async def execute(self) -> Tuple[bool, Optional[str]]: - """执行命令逻辑""" - # 命令执行逻辑 - return True, "执行成功" + async def execute(self) -> Tuple[bool, Optional[str], bool]: + """ + 执行Command的主要逻辑 + + Returns: + Tuple[bool, str, bool]: + - 第一个bool表示是否成功执行 + - 第二个str是执行结果消息 + - 第三个bool表示是否需要阻止消息继续处理 + """ + # ---- 执行命令的逻辑 ---- + return True, "执行成功", False ``` +**`command_pattern`**: 该Command匹配的正则表达式,用于精确匹配用户输入。 -### 属性说明 +请注意:如果希望能获取到命令中的参数,请在正则表达式中使用有命名的捕获组,例如`(?Ppattern)`。 -| 属性 | 类型 | 说明 | -| --------------------- | --------- | -------------------- | -| `command_pattern` | str | 正则表达式匹配模式 | -| `command_help` | str | 命令帮助说明 | -| `command_examples` | List[str] | 使用示例列表 | -| `intercept_message` | bool | 是否拦截消息继续处理 | +这样在匹配时,内部实现可以使用`re.match.groupdict()`方法获取到所有捕获组的参数,并以字典的形式存储在`self.matched_groups`中。 -## 🔍 正则表达式匹配 - -### 基础匹配 +### 匹配样例 +假设我们有一个命令`/example param1=value1 param2=value2`,对应的正则表达式可以是: ```python -class SimpleCommand(BaseCommand): - # 匹配 /ping - command_pattern = r"^/ping$" - - async def execute(self) -> Tuple[bool, Optional[str]]: - await self.send_text("Pong!") - return True, "发送了Pong回复" +class ExampleCommand(BaseCommand): + command_name = "example" + command_description = "这是一个示例命令" + command_pattern = r"/example (?P\w+) (?P\w+)" + + async def execute(self) -> Tuple[bool, Optional[str], bool]: + # 获取匹配的参数 + param1 = self.matched_groups.get("param1") + param2 = self.matched_groups.get("param2") + + # 执行逻辑 + return True, f"参数1: {param1}, 参数2: {param2}", False ``` -### 参数捕获 - -使用命名组 `(?Ppattern)` 捕获参数: +--- +## Command 内置方法说明 ```python -class UserCommand(BaseCommand): - # 匹配 /user add 张三 或 /user del 李四 - command_pattern = r"^/user\s+(?Padd|del|info)\s+(?P\w+)$" - - async def execute(self) -> Tuple[bool, Optional[str]]: - # 通过 self.matched_groups 获取捕获的参数 - action = self.matched_groups.get("action") - username = self.matched_groups.get("username") - - if action == "add": - await self.send_text(f"添加用户:{username}") - elif action == "del": - await self.send_text(f"删除用户:{username}") - elif action == "info": - await self.send_text(f"用户信息:{username}") - - return True, f"执行了{action}操作" +class BaseCommand: + def get_config(self, key: str, default=None): + """获取插件配置值,使用嵌套键访问""" + + async def send_text(self, content: str, reply_to: str = "") -> bool: + """发送回复消息""" + + async def send_type(self, message_type: str, content: str, display_message: str = "", typing: bool = False, reply_to: str = "") -> bool: + """发送指定类型的回复消息到当前聊天环境""" + + async def send_command(self, command_name: str, args: Optional[dict] = None, display_message: str = "", storage_message: bool = True) -> bool: + """发送命令消息""" + + async def send_emoji(self, emoji_base64: str) -> bool: + """发送表情包""" + + async def send_image(self, image_base64: str) -> bool: + """发送图片""" ``` - -### 可选参数 - -```python -class HelpCommand(BaseCommand): - # 匹配 /help 或 /help topic - command_pattern = r"^/help(?:\s+(?P\w+))?$" - - async def execute(self) -> Tuple[bool, Optional[str]]: - topic = self.matched_groups.get("topic") - - if topic: - await self.send_text(f"显示{topic}的帮助") - else: - await self.send_text("显示总体帮助") - - return True, "显示了帮助信息" -``` - -## 🛑 拦截控制详解 - -### 拦截消息 (intercept_message = True) - -```python -class AdminCommand(BaseCommand): - command_pattern = r"^/admin\s+.+" - command_help = "管理员命令" - intercept_message = True # 拦截,不继续处理 - - async def execute(self) -> Tuple[bool, Optional[str]]: - # 执行管理操作 - await self.send_text("执行管理命令") - # 消息不会继续传递给其他组件 - return True, "管理命令执行完成" -``` - -### 不拦截消息 (intercept_message = False) - -```python -class LogCommand(BaseCommand): - command_pattern = r"^/log\s+.+" - command_help = "记录日志" - intercept_message = False # 不拦截,继续处理 - - async def execute(self) -> Tuple[bool, Optional[str]]: - # 记录日志但不阻止后续处理 - await self.send_text("已记录到日志") - # 消息会继续传递,可能触发Action等其他组件 - return True, "日志记录完成" -``` - -### 拦截控制的用途 - -| 场景 | intercept_message | 说明 | -| -------- | ----------------- | -------------------------- | -| 系统命令 | True | 防止命令被当作普通消息处理 | -| 查询命令 | True | 直接返回结果,无需后续处理 | -| 日志命令 | False | 记录但允许消息继续流转 | -| 监控命令 | False | 监控但不影响正常聊天 | - -## 🎨 完整Command示例 - -### 用户管理Command - -```python -from src.plugin_system import BaseCommand -from typing import Tuple, Optional - -class UserManagementCommand(BaseCommand): - """用户管理Command - 展示复杂参数处理""" - - command_pattern = r"^/user\s+(?Padd|del|list|info)\s*(?P\w+)?(?:\s+--(?P.+))?$" - command_help = "用户管理命令,支持添加、删除、列表、信息查询" - command_examples = [ - "/user add 张三", - "/user del 李四", - "/user list", - "/user info 王五", - "/user add 赵六 --role=admin" - ] - intercept_message = True - - async def execute(self) -> Tuple[bool, Optional[str]]: - """执行用户管理命令""" - try: - action = self.matched_groups.get("action") - username = self.matched_groups.get("username") - options = self.matched_groups.get("options") - - # 解析选项 - parsed_options = self._parse_options(options) if options else {} - - if action == "add": - return await self._add_user(username, parsed_options) - elif action == "del": - return await self._delete_user(username) - elif action == "list": - return await self._list_users() - elif action == "info": - return await self._show_user_info(username) - else: - await self.send_text("❌ 不支持的操作") - return False, f"不支持的操作: {action}" - - except Exception as e: - await self.send_text(f"❌ 命令执行失败: {str(e)}") - return False, f"执行失败: {e}" - - def _parse_options(self, options_str: str) -> dict: - """解析命令选项""" - options = {} - if options_str: - for opt in options_str.split(): - if "=" in opt: - key, value = opt.split("=", 1) - options[key] = value - return options - - async def _add_user(self, username: str, options: dict) -> Tuple[bool, str]: - """添加用户""" - if not username: - await self.send_text("❌ 请指定用户名") - return False, "缺少用户名参数" - - # 检查用户是否已存在 - existing_users = await self._get_user_list() - if username in existing_users: - await self.send_text(f"❌ 用户 {username} 已存在") - return False, f"用户已存在: {username}" - - # 添加用户逻辑 - role = options.get("role", "user") - await self.send_text(f"✅ 成功添加用户 {username},角色: {role}") - return True, f"添加用户成功: {username}" - - async def _delete_user(self, username: str) -> Tuple[bool, str]: - """删除用户""" - if not username: - await self.send_text("❌ 请指定用户名") - return False, "缺少用户名参数" - - await self.send_text(f"✅ 用户 {username} 已删除") - return True, f"删除用户成功: {username}" - - async def _list_users(self) -> Tuple[bool, str]: - """列出所有用户""" - users = await self._get_user_list() - if users: - user_list = "\n".join([f"• {user}" for user in users]) - await self.send_text(f"📋 用户列表:\n{user_list}") - else: - await self.send_text("📋 暂无用户") - return True, "显示用户列表" - - async def _show_user_info(self, username: str) -> Tuple[bool, str]: - """显示用户信息""" - if not username: - await self.send_text("❌ 请指定用户名") - return False, "缺少用户名参数" - - # 模拟用户信息 - user_info = f""" -👤 用户信息: {username} -📧 邮箱: {username}@example.com -🕒 注册时间: 2024-01-01 -🎯 角色: 普通用户 - """.strip() - - await self.send_text(user_info) - return True, f"显示用户信息: {username}" - - async def _get_user_list(self) -> list: - """获取用户列表(示例)""" - return ["张三", "李四", "王五"] -``` - -### 系统信息Command - -```python -class SystemInfoCommand(BaseCommand): - """系统信息Command - 展示系统查询功能""" - - command_pattern = r"^/(?:status|info)(?:\s+(?Psystem|memory|plugins|all))?$" - command_help = "查询系统状态信息" - command_examples = [ - "/status", - "/info system", - "/status memory", - "/info plugins" - ] - intercept_message = True - - async def execute(self) -> Tuple[bool, Optional[str]]: - """执行系统信息查询""" - info_type = self.matched_groups.get("type", "all") - - try: - if info_type in ["system", "all"]: - await self._show_system_info() - - if info_type in ["memory", "all"]: - await self._show_memory_info() - - if info_type in ["plugins", "all"]: - await self._show_plugin_info() - - return True, f"显示了{info_type}类型的系统信息" - - except Exception as e: - await self.send_text(f"❌ 获取系统信息失败: {str(e)}") - return False, f"查询失败: {e}" - - async def _show_system_info(self): - """显示系统信息""" - import platform - import datetime - - system_info = f""" -🖥️ **系统信息** -📱 平台: {platform.system()} {platform.release()} -🐍 Python: {platform.python_version()} -⏰ 运行时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - """.strip() - - await self.send_text(system_info) - - async def _show_memory_info(self): - """显示内存信息""" - import psutil - - memory = psutil.virtual_memory() - memory_info = f""" -💾 **内存信息** -📊 总内存: {memory.total // (1024**3)} GB -🟢 可用内存: {memory.available // (1024**3)} GB -📈 使用率: {memory.percent}% - """.strip() - - await self.send_text(memory_info) - - async def _show_plugin_info(self): - """显示插件信息""" - # 通过配置获取插件信息 - plugins = await self._get_loaded_plugins() - - plugin_info = f""" -🔌 **插件信息** -📦 已加载插件: {len(plugins)} -🔧 活跃插件: {len([p for p in plugins if p.get('active', False)])} - """.strip() - - await self.send_text(plugin_info) - - async def _get_loaded_plugins(self) -> list: - """获取已加载的插件列表""" - # 这里可以通过配置或API获取实际的插件信息 - return [ - {"name": "core_actions", "active": True}, - {"name": "example_plugin", "active": True}, - ] -``` - -### 自定义前缀Command - -```python -class CustomPrefixCommand(BaseCommand): - """自定义前缀Command - 展示非/前缀的命令""" - - # 使用!前缀而不是/前缀 - command_pattern = r"^[!!](?Proll|dice)\s*(?P\d+)?$" - command_help = "骰子命令,使用!前缀" - command_examples = ["!roll", "!dice 6", "!roll 20"] - intercept_message = True - - async def execute(self) -> Tuple[bool, Optional[str]]: - """执行骰子命令""" - import random - - command = self.matched_groups.get("command") - count = int(self.matched_groups.get("count", "6")) - - # 限制骰子面数 - if count > 100: - await self.send_text("❌ 骰子面数不能超过100") - return False, "骰子面数超限" - - result = random.randint(1, count) - await self.send_text(f"🎲 投掷{count}面骰子,结果: {result}") - - return True, f"投掷了{count}面骰子,结果{result}" -``` - -## 📊 性能优化建议 - -### 1. 正则表达式优化 - -```python -# ✅ 好的做法 - 简单直接 -command_pattern = r"^/ping$" - -# ❌ 避免 - 过于复杂 -command_pattern = r"^/(?:ping|pong|test|check|status|info|help|...)" - -# ✅ 好的做法 - 分离复杂逻辑 -``` - -### 2. 参数验证 - -```python -# ✅ 好的做法 - 早期验证 -async def execute(self) -> Tuple[bool, Optional[str]]: - username = self.matched_groups.get("username") - if not username: - await self.send_text("❌ 请提供用户名") - return False, "缺少参数" - - # 继续处理... -``` - -### 3. 错误处理 - -```python -# ✅ 好的做法 - 完整错误处理 -async def execute(self) -> Tuple[bool, Optional[str]]: - try: - # 主要逻辑 - result = await self._process_command() - return True, "执行成功" - except ValueError as e: - await self.send_text(f"❌ 参数错误: {e}") - return False, f"参数错误: {e}" - except Exception as e: - await self.send_text(f"❌ 执行失败: {e}") - return False, f"执行失败: {e}" -``` - -## 🎯 最佳实践 - -### 1. 命令设计原则 - -```python -# ✅ 好的命令设计 -"/user add 张三" # 动作 + 对象 + 参数 -"/config set key=value" # 动作 + 子动作 + 参数 -"/help command" # 动作 + 可选参数 - -# ❌ 避免的设计 -"/add_user_with_name_张三" # 过于冗长 -"/u a 张三" # 过于简写 -``` - -### 2. 帮助信息 - -```python -class WellDocumentedCommand(BaseCommand): - command_pattern = r"^/example\s+(?P\w+)$" - command_help = "示例命令:处理指定参数并返回结果" - command_examples = [ - "/example test", - "/example debug", - "/example production" - ] -``` - -### 3. 错误处理 - -```python -async def execute(self) -> Tuple[bool, Optional[str]]: - param = self.matched_groups.get("param") - - # 参数验证 - if param not in ["test", "debug", "production"]: - await self.send_text("❌ 无效的参数,支持: test, debug, production") - return False, "无效参数" - - # 执行逻辑 - try: - result = await self._process_param(param) - await self.send_text(f"✅ 处理完成: {result}") - return True, f"处理{param}成功" - except Exception as e: - await self.send_text("❌ 处理失败,请稍后重试") - return False, f"处理失败: {e}" -``` - -### 4. 配置集成 - -```python -async def execute(self) -> Tuple[bool, Optional[str]]: - # 从配置读取设置 - max_items = self.get_config("command.max_items", 10) - timeout = self.get_config("command.timeout", 30) - - # 使用配置进行处理 - ... -``` - -## 📝 Command vs Action 选择指南 - -### 使用Command的场景 - -- ✅ 用户需要明确调用特定功能 -- ✅ 需要精确的参数控制 -- ✅ 管理和配置操作 -- ✅ 查询和信息显示 -- ✅ 系统维护命令 - -### 使用Action的场景 - -- ✅ 增强麦麦的智能行为 -- ✅ 根据上下文自动触发 -- ✅ 情绪和表情表达 -- ✅ 智能建议和帮助 -- ✅ 随机化的互动 - - +具体参数与用法参见`BaseCommand`基类的定义。 \ No newline at end of file From 37bf904c4571b8e20cf5281efe795e50707dc13d Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Fri, 25 Jul 2025 14:33:33 +0800 Subject: [PATCH 15/26] =?UTF-8?q?tools=20=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plugins/tool-system.md | 201 +++--------------------------------- 1 file changed, 12 insertions(+), 189 deletions(-) diff --git a/docs/plugins/tool-system.md b/docs/plugins/tool-system.md index d9093c89..baa43528 100644 --- a/docs/plugins/tool-system.md +++ b/docs/plugins/tool-system.md @@ -2,12 +2,11 @@ ## 📖 什么是工具系统 -工具系统是MaiBot的信息获取能力扩展组件,**专门用于在Focus模式下扩宽麦麦能够获得的信息量**。如果说Action组件功能五花八门,可以拓展麦麦能做的事情,那么Tool就是在某个过程中拓宽了麦麦能够获得的信息量。 +工具系统是MaiBot的信息获取能力扩展组件。如果说Action组件功能五花八门,可以拓展麦麦能做的事情,那么Tool就是在某个过程中拓宽了麦麦能够获得的信息量。 ### 🎯 工具系统的特点 - 🔍 **信息获取增强**:扩展麦麦获取外部信息的能力 -- 🎯 **Focus模式专用**:仅在专注聊天模式下工作,必须开启工具处理器 - 📊 **数据丰富**:帮助麦麦获得更多背景信息和实时数据 - 🔌 **插件式架构**:支持独立开发和注册新工具 - ⚡ **自动发现**:工具会被系统自动识别和注册 @@ -17,7 +16,6 @@ | 特征 | Action | Command | Tool | |-----|-------|---------|------| | **主要用途** | 扩展麦麦行为能力 | 响应用户指令 | 扩展麦麦信息获取 | -| **适用模式** | 所有模式 | 所有模式 | 仅Focus模式 | | **触发方式** | 麦麦智能决策 | 用户主动触发 | LLM根据需要调用 | | **目标** | 让麦麦做更多事情 | 提供具体功能 | 让麦麦知道更多信息 | | **使用场景** | 增强交互体验 | 功能服务 | 信息查询和分析 | @@ -54,7 +52,7 @@ class MyTool(BaseTool): "required": ["query"] } - async def execute(self, function_args, message_txt=""): + async def execute(self, function_args: Dict[str, Any]): """执行工具逻辑""" # 实现工具功能 result = f"查询结果: {function_args.get('query')}" @@ -63,9 +61,6 @@ class MyTool(BaseTool): "name": self.name, "content": result } - -# 注册工具 -register_tool(MyTool) ``` ### 属性说明 @@ -80,7 +75,7 @@ register_tool(MyTool) | 方法 | 参数 | 返回值 | 说明 | |-----|------|--------|------| -| `execute` | `function_args`, `message_txt` | `dict` | 执行工具核心逻辑 | +| `execute` | `function_args` | `dict` | 执行工具核心逻辑 | ## 🔄 自动注册机制 @@ -88,28 +83,14 @@ register_tool(MyTool) 1. **文件扫描**:系统自动遍历 `tool_can_use` 目录中的所有Python文件 2. **类识别**:寻找继承自 `BaseTool` 的工具类 -3. **自动注册**:调用 `register_tool()` 的工具会被注册到系统中 +3. **自动注册**:只需要实现对应的类并把文件放在正确文件夹中就可自动注册 4. **即用即加载**:工具在需要时被实例化和调用 -### 注册流程 - -```python -# 1. 创建工具类 -class WeatherTool(BaseTool): - name = "weather_query" - description = "查询指定城市的天气信息" - # ... - -# 2. 注册工具(在文件末尾) -register_tool(WeatherTool) - -# 3. 系统自动发现(无需手动操作) -# discover_tools() 函数会自动完成注册 -``` +--- ## 🎨 完整工具示例 -### 天气查询工具 +完成一个天气查询工具 ```python from src.tools.tool_can_use.base_tool import BaseTool, register_tool @@ -192,102 +173,9 @@ class WeatherTool(BaseTool): 💧 湿度: {humidity}% ━━━━━━━━━━━━━━━━━━ """.strip() - -# 注册工具 -register_tool(WeatherTool) ``` -### 知识查询工具 - -```python -from src.tools.tool_can_use.base_tool import BaseTool, register_tool - -class KnowledgeSearchTool(BaseTool): - """知识搜索工具 - 查询百科知识和专业信息""" - - name = "knowledge_search" - description = "搜索百科知识、专业术语解释、历史事件等信息" - - parameters = { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "要搜索的知识关键词或问题" - }, - "category": { - "type": "string", - "description": "知识分类:science(科学)、history(历史)、technology(技术)、general(通用)等", - "enum": ["science", "history", "technology", "general"] - }, - "language": { - "type": "string", - "description": "结果语言:zh(中文)、en(英文)", - "enum": ["zh", "en"] - } - }, - "required": ["query"] - } - - async def execute(self, function_args, message_txt=""): - """执行知识搜索""" - try: - query = function_args.get("query") - category = function_args.get("category", "general") - language = function_args.get("language", "zh") - - # 执行搜索逻辑 - search_results = await self._search_knowledge(query, category, language) - - # 格式化结果 - result = self._format_search_results(query, search_results) - - return { - "name": self.name, - "content": result - } - - except Exception as e: - return { - "name": self.name, - "content": f"知识搜索失败: {str(e)}" - } - - async def _search_knowledge(self, query: str, category: str, language: str) -> list: - """执行知识搜索""" - # 这里实现实际的搜索逻辑 - # 可以对接维基百科API、百度百科API等 - - # 示例返回数据 - return [ - { - "title": f"{query}的定义", - "summary": f"关于{query}的详细解释...", - "source": "Wikipedia" - } - ] - - def _format_search_results(self, query: str, results: list) -> str: - """格式化搜索结果""" - if not results: - return f"未找到关于 '{query}' 的相关信息" - - formatted_text = f"📚 关于 '{query}' 的搜索结果:\n\n" - - for i, result in enumerate(results[:3], 1): # 限制显示前3条 - title = result.get("title", "无标题") - summary = result.get("summary", "无摘要") - source = result.get("source", "未知来源") - - formatted_text += f"{i}. **{title}**\n" - formatted_text += f" {summary}\n" - formatted_text += f" 📖 来源: {source}\n\n" - - return formatted_text.strip() - -# 注册工具 -register_tool(KnowledgeSearchTool) -``` +--- ## 📊 工具开发步骤 @@ -323,86 +211,21 @@ class MyNewTool(BaseTool): "name": self.name, "content": "执行结果" } - -register_tool(MyNewTool) ``` -### 3. 测试工具 - -创建测试文件验证工具功能: - -```python -import asyncio -from my_new_tool import MyNewTool - -async def test_tool(): - tool = MyNewTool() - result = await tool.execute({"param": "value"}) - print(result) - -asyncio.run(test_tool()) -``` - -### 4. 系统集成 +### 3. 系统集成 工具创建完成后,系统会自动发现和注册,无需额外配置。 -## ⚙️ 工具处理器配置 - -### 启用工具处理器 - -工具系统仅在Focus模式下工作,需要确保工具处理器已启用: - -```python -# 在Focus模式配置中 -focus_config = { - "enable_tool_processor": True, # 必须启用 - "tool_timeout": 30, # 工具执行超时时间(秒) - "max_tools_per_message": 3 # 单次消息最大工具调用数 -} -``` - -### 工具使用流程 - -1. **用户发送消息**:在Focus模式下发送需要信息查询的消息 -2. **LLM判断需求**:麦麦分析消息,判断是否需要使用工具获取信息 -3. **选择工具**:根据需求选择合适的工具 -4. **调用工具**:执行工具获取信息 -5. **整合回复**:将工具获取的信息整合到回复中 - -### 使用示例 - -```python -# 用户消息示例 -"今天北京的天气怎么样?" - -# 系统处理流程: -# 1. 麦麦识别这是天气查询需求 -# 2. 调用 weather_query 工具 -# 3. 获取北京天气信息 -# 4. 整合信息生成回复 - -# 最终回复: -"根据最新天气数据,北京今天晴天,温度22°C,湿度45%,适合外出活动。" -``` +--- ## 🚨 注意事项和限制 ### 当前限制 -1. **模式限制**:仅在Focus模式下可用 -2. **独立开发**:需要单独编写,暂未完全融入插件系统 -3. **适用范围**:主要适用于信息获取场景 -4. **配置要求**:必须开启工具处理器 - -### 未来改进 - -工具系统在之后可能会面临以下修改: - -1. **插件系统融合**:更好地集成到插件系统中 -2. **模式扩展**:可能扩展到其他聊天模式 -3. **配置简化**:简化配置和部署流程 -4. **性能优化**:提升工具调用效率 +1. **独立开发**:需要单独编写,暂未完全融入插件系统 +2. **适用范围**:主要适用于信息获取场景 +3. **配置要求**:必须开启工具处理器 ### 开发建议 From 3a4f343d8412e2abe75b83fb3e58615efe2e90ba Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Fri, 25 Jul 2025 14:35:59 +0800 Subject: [PATCH 16/26] =?UTF-8?q?tools=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugin_system/base/base_command.py | 4 ++-- src/tools/tool_can_use/compare_numbers_tool.py | 8 ++------ src/tools/tool_can_use/rename_person_tool.py | 7 +++---- src/tools/tool_executor.py | 4 ++-- src/tools/tool_use.py | 3 ++- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/plugin_system/base/base_command.py b/src/plugin_system/base/base_command.py index 60ee99ad..652acb4c 100644 --- a/src/plugin_system/base/base_command.py +++ b/src/plugin_system/base/base_command.py @@ -60,10 +60,10 @@ class BaseCommand(ABC): pass def get_config(self, key: str, default=None): - """获取插件配置值,支持嵌套键访问 + """获取插件配置值,使用嵌套键访问 Args: - key: 配置键名,支持嵌套访问如 "section.subsection.key" + key: 配置键名,使用嵌套访问如 "section.subsection.key" default: 默认值 Returns: diff --git a/src/tools/tool_can_use/compare_numbers_tool.py b/src/tools/tool_can_use/compare_numbers_tool.py index 2930f8f4..236a4587 100644 --- a/src/tools/tool_can_use/compare_numbers_tool.py +++ b/src/tools/tool_can_use/compare_numbers_tool.py @@ -39,11 +39,7 @@ class CompareNumbersTool(BaseTool): else: result = f"{num1} 等于 {num2}" - return {"type": "comparison_result", "id": f"{num1}_vs_{num2}", "content": result} + return {"name": self.name, "content": result} except Exception as e: logger.error(f"比较数字失败: {str(e)}") - return {"type": "info", "id": f"{num1}_vs_{num2}", "content": f"比较数字失败,炸了: {str(e)}"} - - -# 注册工具 -# register_tool(CompareNumbersTool) + return {"name": self.name, "content": f"比较数字失败,炸了: {str(e)}"} diff --git a/src/tools/tool_can_use/rename_person_tool.py b/src/tools/tool_can_use/rename_person_tool.py index 2216b824..17e62468 100644 --- a/src/tools/tool_can_use/rename_person_tool.py +++ b/src/tools/tool_can_use/rename_person_tool.py @@ -1,7 +1,6 @@ from src.tools.tool_can_use.base_tool import BaseTool from src.person_info.person_info import get_person_info_manager from src.common.logger import get_logger -import time logger = get_logger("rename_person_tool") @@ -24,7 +23,7 @@ class RenamePersonTool(BaseTool): "required": ["person_name"], } - async def execute(self, function_args: dict, message_txt=""): + async def execute(self, function_args: dict): """ 执行取名工具逻辑 @@ -82,7 +81,7 @@ class RenamePersonTool(BaseTool): content = f"已成功将用户 {person_name_to_find} 的备注名更新为 {new_name}" logger.info(content) - return {"type": "info", "id": f"rename_success_{time.time()}", "content": content} + return {"name": self.name, "content": content} else: logger.warning(f"为用户 {person_id} 调用 qv_person_name 后未能成功获取新昵称。") # 尝试从内存中获取可能已经更新的名字 @@ -101,4 +100,4 @@ class RenamePersonTool(BaseTool): except Exception as e: error_msg = f"重命名失败: {str(e)}" logger.error(error_msg, exc_info=True) - return {"type": "info_error", "id": f"rename_error_{time.time()}", "content": error_msg} + return {"name": self.name, "content": error_msg} diff --git a/src/tools/tool_executor.py b/src/tools/tool_executor.py index 403ed554..0f50ca2a 100644 --- a/src/tools/tool_executor.py +++ b/src/tools/tool_executor.py @@ -172,7 +172,7 @@ class ToolExecutor: logger.debug(f"{self.log_prefix}执行工具: {tool_name}") # 执行工具 - result = await self.tool_instance._execute_tool_call(tool_call) + result = await self.tool_instance.execute_tool_call(tool_call) if result: tool_info = { @@ -299,7 +299,7 @@ class ToolExecutor: logger.info(f"{self.log_prefix}直接执行工具: {tool_name}") - result = await self.tool_instance._execute_tool_call(tool_call) + result = await self.tool_instance.execute_tool_call(tool_call) if result: tool_info = { diff --git a/src/tools/tool_use.py b/src/tools/tool_use.py index 738eeed4..6a8cd48a 100644 --- a/src/tools/tool_use.py +++ b/src/tools/tool_use.py @@ -16,7 +16,8 @@ class ToolUser: return get_all_tool_definitions() @staticmethod - async def _execute_tool_call(tool_call): + async def execute_tool_call(tool_call): + # sourcery skip: use-assigned-variable """执行特定的工具调用 Args: From 5182609ca433d7a6597c0f5f85d64f7b5b484d47 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Fri, 25 Jul 2025 14:47:40 +0800 Subject: [PATCH 17/26] =?UTF-8?q?manifest=E8=AF=B4=E6=98=8E=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plugins/manifest-guide.md | 21 ++++++--------------- src/plugin_system/utils/manifest_utils.py | 15 +++++---------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/docs/plugins/manifest-guide.md b/docs/plugins/manifest-guide.md index 5c5d7e3f..d3dd746a 100644 --- a/docs/plugins/manifest-guide.md +++ b/docs/plugins/manifest-guide.md @@ -147,7 +147,7 @@ python scripts/manifest_tool.py validate src/plugins/my_plugin ## 📋 字段说明 ### 基本信息 -- `manifest_version`: manifest格式版本,当前为3 +- `manifest_version`: manifest格式版本,当前为1 - `name`: 插件显示名称(必需) - `version`: 插件版本号(必需) - `description`: 插件功能描述(必需) @@ -165,10 +165,12 @@ python scripts/manifest_tool.py validate src/plugins/my_plugin - `categories`: 分类数组(可选,建议填写) ### 兼容性 -- `host_application`: 主机应用兼容性(可选) +- `host_application`: 主机应用兼容性(可选,建议填写) - `min_version`: 最低兼容版本 - `max_version`: 最高兼容版本 +⚠️ 在不填写的情况下,插件将默认支持所有版本。**(由于我们在不同版本对插件系统进行了大量的重构,这种情况几乎不可能。)** + ### 国际化 - `default_locale`: 默认语言(可选) - `locales_path`: 语言文件目录(可选) @@ -185,24 +187,13 @@ python scripts/manifest_tool.py validate src/plugins/my_plugin 2. **编码格式**:manifest文件必须使用UTF-8编码 3. **JSON格式**:文件必须是有效的JSON格式 4. **必需字段**:`manifest_version`、`name`、`version`、`description`、`author.name`是必需的 -5. **版本兼容**:当前只支持manifest_version = 3 +5. **版本兼容**:当前只支持`manifest_version = 1` ## 🔍 常见问题 -### Q: 为什么要强制要求manifest文件? -A: Manifest文件提供了插件的标准化元数据,使得插件管理、依赖检查、版本兼容性验证等功能成为可能。 - ### Q: 可以不填写可选字段吗? A: 可以。所有标记为"可选"的字段都可以不填写,但建议至少填写`license`和`keywords`。 -### Q: 如何快速为所有插件创建manifest? -A: 可以编写脚本批量处理: -```bash -# 扫描并为每个缺少manifest的插件创建最小化manifest -python scripts/manifest_tool.py scan src/plugins -# 然后手动为每个插件运行create-minimal命令 -``` - ### Q: manifest验证失败怎么办? A: 根据验证器的错误提示修复相应问题。错误会导致插件加载失败,警告不会。 @@ -210,5 +201,5 @@ A: 根据验证器的错误提示修复相应问题。错误会导致插件加 查看内置插件的manifest文件作为参考: - `src/plugins/built_in/core_actions/_manifest.json` -- `src/plugins/built_in/doubao_pic_plugin/_manifest.json` - `src/plugins/built_in/tts_plugin/_manifest.json` +- `src/plugins/hello_world_plugin/_manifest.json` diff --git a/src/plugin_system/utils/manifest_utils.py b/src/plugin_system/utils/manifest_utils.py index 6a8aa804..d070b733 100644 --- a/src/plugin_system/utils/manifest_utils.py +++ b/src/plugin_system/utils/manifest_utils.py @@ -163,12 +163,11 @@ class VersionComparator: version_normalized, max_normalized ) - if is_compatible: - logger.info(f"版本兼容性检查:{compat_msg}") - return True, compat_msg - else: + if not is_compatible: return False, f"版本 {version_normalized} 高于最大支持版本 {max_normalized},且无兼容性映射" + logger.info(f"版本兼容性检查:{compat_msg}") + return True, compat_msg return True, "" @staticmethod @@ -358,14 +357,10 @@ class ManifestValidator: if self.validation_errors: report.append("❌ 验证错误:") - for error in self.validation_errors: - report.append(f" - {error}") - + report.extend(f" - {error}" for error in self.validation_errors) if self.validation_warnings: report.append("⚠️ 验证警告:") - for warning in self.validation_warnings: - report.append(f" - {warning}") - + report.extend(f" - {warning}" for warning in self.validation_warnings) if not self.validation_errors and not self.validation_warnings: report.append("✅ Manifest文件验证通过") From 27b1ce047cef0680ca4d49e6e6fbd632e52030bb Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Fri, 25 Jul 2025 15:23:57 +0800 Subject: [PATCH 18/26] =?UTF-8?q?workflow=E4=B8=B4=E6=97=B6=E6=9B=B4?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-image.yml | 19 ++++++++++++------- .github/workflows/ruff.yml | 12 ++++++------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 47fdf5b7..36f5ba8f 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -1,16 +1,21 @@ name: Docker Build and Push on: - push: + # push: + # branches: + # - main + # - classical + # - dev + # tags: + # - "v*.*.*" + # - "v*" + # - "*.*.*" + # - "*.*.*-*" + workflow_dispatch: # 允许手动触发工作流 branches: - main - - classical - dev - tags: - - "v*.*.*" - - "v*" - - "*.*.*" - - "*.*.*-*" + - dev-refactor # Workflow's jobs jobs: diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 66140d74..3d2e7d1f 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -1,12 +1,12 @@ name: Ruff on: - push: - branches: - - main - - dev - - dev-refactor # 例如:匹配所有以 feature/ 开头的分支 - # 添加你希望触发此 workflow 的其他分支 + # push: + # branches: + # - main + # - dev + # - dev-refactor # 例如:匹配所有以 feature/ 开头的分支 + # # 添加你希望触发此 workflow 的其他分支 workflow_dispatch: # 允许手动触发工作流 branches: - main From 8c9b2b54c0aab4e5003945fb759f83f895b77003 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Jul 2025 15:45:14 +0800 Subject: [PATCH 19/26] =?UTF-8?q?better=EF=BC=9A=E4=BC=98=E5=8C=96interest?= =?UTF-8?q?=E7=9A=84=E7=AE=97=E6=B3=95=EF=BC=8C=E6=9B=B4=E5=A5=BD=E6=9B=B4?= =?UTF-8?q?=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/text_length_analysis.py | 394 ++++++++++++++++++ .../heart_flow/heartflow_message_processor.py | 34 +- src/chat/memory_system/Hippocampus.py | 124 +++--- src/mais4u/mais4u_chat/s4u_msg_processor.py | 34 +- 4 files changed, 511 insertions(+), 75 deletions(-) create mode 100644 scripts/text_length_analysis.py diff --git a/scripts/text_length_analysis.py b/scripts/text_length_analysis.py new file mode 100644 index 00000000..2ca596e2 --- /dev/null +++ b/scripts/text_length_analysis.py @@ -0,0 +1,394 @@ +import time +import sys +import os +import re +from typing import Dict, List, Tuple, Optional +from datetime import datetime +# Add project root to Python path +project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, project_root) +from src.common.database.database_model import Messages, ChatStreams #noqa + + +def contains_emoji_or_image_tags(text: str) -> bool: + """Check if text contains [表情包xxxxx] or [图片xxxxx] tags""" + if not text: + return False + + # 检查是否包含 [表情包] 或 [图片] 标记 + emoji_pattern = r'\[表情包[^\]]*\]' + image_pattern = r'\[图片[^\]]*\]' + + return bool(re.search(emoji_pattern, text) or re.search(image_pattern, text)) + + +def clean_reply_text(text: str) -> str: + """Remove reply references like [回复 xxxx...] from text""" + if not text: + return text + + # 匹配 [回复 xxxx...] 格式的内容 + # 使用非贪婪匹配,匹配到第一个 ] 就停止 + cleaned_text = re.sub(r'\[回复[^\]]*\]', '', text) + + # 去除多余的空白字符 + cleaned_text = cleaned_text.strip() + + return cleaned_text + + +def get_chat_name(chat_id: str) -> str: + """Get chat name from chat_id by querying ChatStreams table directly""" + try: + chat_stream = ChatStreams.get_or_none(ChatStreams.stream_id == chat_id) + if chat_stream is None: + return f"未知聊天 ({chat_id})" + + if chat_stream.group_name: + return f"{chat_stream.group_name} ({chat_id})" + elif chat_stream.user_nickname: + return f"{chat_stream.user_nickname}的私聊 ({chat_id})" + else: + return f"未知聊天 ({chat_id})" + except Exception: + return f"查询失败 ({chat_id})" + + +def format_timestamp(timestamp: float) -> str: + """Format timestamp to readable date string""" + try: + return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") + except (ValueError, OSError): + return "未知时间" + + +def calculate_text_length_distribution(messages) -> Dict[str, int]: + """Calculate distribution of processed_plain_text length""" + distribution = { + '0': 0, # 空文本 + '1-5': 0, # 极短文本 + '6-10': 0, # 很短文本 + '11-20': 0, # 短文本 + '21-30': 0, # 较短文本 + '31-50': 0, # 中短文本 + '51-70': 0, # 中等文本 + '71-100': 0, # 较长文本 + '101-150': 0, # 长文本 + '151-200': 0, # 很长文本 + '201-300': 0, # 超长文本 + '301-500': 0, # 极长文本 + '501-1000': 0, # 巨长文本 + '1000+': 0 # 超巨长文本 + } + + for msg in messages: + if msg.processed_plain_text is None: + continue + + # 排除包含表情包或图片标记的消息 + if contains_emoji_or_image_tags(msg.processed_plain_text): + continue + + # 清理文本中的回复引用 + cleaned_text = clean_reply_text(msg.processed_plain_text) + length = len(cleaned_text) + + if length == 0: + distribution['0'] += 1 + elif length <= 5: + distribution['1-5'] += 1 + elif length <= 10: + distribution['6-10'] += 1 + elif length <= 20: + distribution['11-20'] += 1 + elif length <= 30: + distribution['21-30'] += 1 + elif length <= 50: + distribution['31-50'] += 1 + elif length <= 70: + distribution['51-70'] += 1 + elif length <= 100: + distribution['71-100'] += 1 + elif length <= 150: + distribution['101-150'] += 1 + elif length <= 200: + distribution['151-200'] += 1 + elif length <= 300: + distribution['201-300'] += 1 + elif length <= 500: + distribution['301-500'] += 1 + elif length <= 1000: + distribution['501-1000'] += 1 + else: + distribution['1000+'] += 1 + + return distribution + + +def get_text_length_stats(messages) -> Dict[str, float]: + """Calculate basic statistics for processed_plain_text length""" + lengths = [] + null_count = 0 + excluded_count = 0 # 被排除的消息数量 + + for msg in messages: + if msg.processed_plain_text is None: + null_count += 1 + elif contains_emoji_or_image_tags(msg.processed_plain_text): + # 排除包含表情包或图片标记的消息 + excluded_count += 1 + else: + # 清理文本中的回复引用 + cleaned_text = clean_reply_text(msg.processed_plain_text) + lengths.append(len(cleaned_text)) + + if not lengths: + return { + 'count': 0, + 'null_count': null_count, + 'excluded_count': excluded_count, + 'min': 0, + 'max': 0, + 'avg': 0, + 'median': 0 + } + + lengths.sort() + count = len(lengths) + + return { + 'count': count, + 'null_count': null_count, + 'excluded_count': excluded_count, + 'min': min(lengths), + 'max': max(lengths), + 'avg': sum(lengths) / count, + 'median': lengths[count // 2] if count % 2 == 1 else (lengths[count // 2 - 1] + lengths[count // 2]) / 2 + } + + +def get_available_chats() -> List[Tuple[str, str, int]]: + """Get all available chats with message counts""" + try: + # 获取所有有消息的chat_id,排除特殊类型消息 + chat_counts = {} + for msg in Messages.select(Messages.chat_id).distinct(): + chat_id = msg.chat_id + count = Messages.select().where( + (Messages.chat_id == chat_id) & + (Messages.is_emoji != 1) & + (Messages.is_picid != 1) & + (Messages.is_command != 1) + ).count() + if count > 0: + chat_counts[chat_id] = count + + # 获取聊天名称 + result = [] + for chat_id, count in chat_counts.items(): + chat_name = get_chat_name(chat_id) + result.append((chat_id, chat_name, count)) + + # 按消息数量排序 + result.sort(key=lambda x: x[2], reverse=True) + return result + except Exception as e: + print(f"获取聊天列表失败: {e}") + return [] + + +def get_time_range_input() -> Tuple[Optional[float], Optional[float]]: + """Get time range input from user""" + print("\n时间范围选择:") + print("1. 最近1天") + print("2. 最近3天") + print("3. 最近7天") + print("4. 最近30天") + print("5. 自定义时间范围") + print("6. 不限制时间") + + choice = input("请选择时间范围 (1-6): ").strip() + + now = time.time() + + if choice == "1": + return now - 24*3600, now + elif choice == "2": + return now - 3*24*3600, now + elif choice == "3": + return now - 7*24*3600, now + elif choice == "4": + return now - 30*24*3600, now + elif choice == "5": + print("请输入开始时间 (格式: YYYY-MM-DD HH:MM:SS):") + start_str = input().strip() + print("请输入结束时间 (格式: YYYY-MM-DD HH:MM:SS):") + end_str = input().strip() + + try: + start_time = datetime.strptime(start_str, "%Y-%m-%d %H:%M:%S").timestamp() + end_time = datetime.strptime(end_str, "%Y-%m-%d %H:%M:%S").timestamp() + return start_time, end_time + except ValueError: + print("时间格式错误,将不限制时间范围") + return None, None + else: + return None, None + + +def get_top_longest_messages(messages, top_n: int = 10) -> List[Tuple[str, int, str, str]]: + """Get top N longest messages""" + message_lengths = [] + + for msg in messages: + if msg.processed_plain_text is not None: + # 排除包含表情包或图片标记的消息 + if contains_emoji_or_image_tags(msg.processed_plain_text): + continue + + # 清理文本中的回复引用 + cleaned_text = clean_reply_text(msg.processed_plain_text) + length = len(cleaned_text) + chat_name = get_chat_name(msg.chat_id) + time_str = format_timestamp(msg.time) + # 截取前100个字符作为预览 + preview = cleaned_text[:100] + "..." if len(cleaned_text) > 100 else cleaned_text + message_lengths.append((chat_name, length, time_str, preview)) + + # 按长度排序,取前N个 + message_lengths.sort(key=lambda x: x[1], reverse=True) + return message_lengths[:top_n] + + +def analyze_text_lengths(chat_id: Optional[str] = None, start_time: Optional[float] = None, end_time: Optional[float] = None) -> None: + """Analyze processed_plain_text lengths with optional filters""" + + # 构建查询条件,排除特殊类型的消息 + query = Messages.select().where( + (Messages.is_emoji != 1) & + (Messages.is_picid != 1) & + (Messages.is_command != 1) + ) + + if chat_id: + query = query.where(Messages.chat_id == chat_id) + + if start_time: + query = query.where(Messages.time >= start_time) + + if end_time: + query = query.where(Messages.time <= end_time) + + messages = list(query) + + if not messages: + print("没有找到符合条件的消息") + return + + # 计算统计信息 + distribution = calculate_text_length_distribution(messages) + stats = get_text_length_stats(messages) + top_longest = get_top_longest_messages(messages, 10) + + # 显示结果 + print("\n=== Processed Plain Text 长度分析结果 ===") + print("(已排除表情、图片ID、命令类型消息,已排除[表情包]和[图片]标记消息,已清理回复引用)") + if chat_id: + print(f"聊天: {get_chat_name(chat_id)}") + else: + print("聊天: 全部聊天") + + if start_time and end_time: + print(f"时间范围: {format_timestamp(start_time)} 到 {format_timestamp(end_time)}") + elif start_time: + print(f"时间范围: {format_timestamp(start_time)} 之后") + elif end_time: + print(f"时间范围: {format_timestamp(end_time)} 之前") + else: + print("时间范围: 不限制") + + print("\n基本统计:") + print(f"总消息数量: {len(messages)}") + print(f"有文本消息数量: {stats['count']}") + print(f"空文本消息数量: {stats['null_count']}") + print(f"被排除的消息数量: {stats['excluded_count']}") + if stats['count'] > 0: + print(f"最短长度: {stats['min']} 字符") + print(f"最长长度: {stats['max']} 字符") + print(f"平均长度: {stats['avg']:.2f} 字符") + print(f"中位数长度: {stats['median']:.2f} 字符") + + print("\n文本长度分布:") + total = stats['count'] + if total > 0: + for range_name, count in distribution.items(): + if count > 0: + percentage = count / total * 100 + print(f"{range_name} 字符: {count} ({percentage:.2f}%)") + + # 显示最长的消息 + if top_longest: + print(f"\n最长的 {len(top_longest)} 条消息:") + for i, (chat_name, length, time_str, preview) in enumerate(top_longest, 1): + print(f"{i}. [{chat_name}] {time_str}") + print(f" 长度: {length} 字符") + print(f" 预览: {preview}") + print() + + +def interactive_menu() -> None: + """Interactive menu for text length analysis""" + + while True: + print("\n" + "="*50) + print("Processed Plain Text 长度分析工具") + print("="*50) + print("1. 分析全部聊天") + print("2. 选择特定聊天分析") + print("q. 退出") + + choice = input("\n请选择分析模式 (1-2, q): ").strip() + + if choice.lower() == 'q': + print("再见!") + break + + chat_id = None + + if choice == "2": + # 显示可用的聊天列表 + chats = get_available_chats() + if not chats: + print("没有找到聊天数据") + continue + + print(f"\n可用的聊天 (共{len(chats)}个):") + for i, (_cid, name, count) in enumerate(chats, 1): + print(f"{i}. {name} ({count}条消息)") + + try: + chat_choice = int(input(f"\n请选择聊天 (1-{len(chats)}): ").strip()) + if 1 <= chat_choice <= len(chats): + chat_id = chats[chat_choice - 1][0] + else: + print("无效选择") + continue + except ValueError: + print("请输入有效数字") + continue + + elif choice != "1": + print("无效选择") + continue + + # 获取时间范围 + start_time, end_time = get_time_range_input() + + # 执行分析 + analyze_text_lengths(chat_id, start_time, end_time) + + input("\n按回车键继续...") + + +if __name__ == "__main__": + interactive_menu() \ No newline at end of file diff --git a/src/chat/heart_flow/heartflow_message_processor.py b/src/chat/heart_flow/heartflow_message_processor.py index 32a36568..57b52ae6 100644 --- a/src/chat/heart_flow/heartflow_message_processor.py +++ b/src/chat/heart_flow/heartflow_message_processor.py @@ -61,11 +61,35 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool]: logger.debug(f"记忆激活率: {interested_rate:.2f}") text_len = len(message.processed_plain_text) - # 根据文本长度调整兴趣度,长度越大兴趣度越高,但增长率递减,最低0.01,最高0.05 - # 采用对数函数实现递减增长 - - base_interest = 0.01 + (0.05 - 0.01) * (math.log10(text_len + 1) / math.log10(1000 + 1)) - base_interest = min(max(base_interest, 0.01), 0.05) + # 根据文本长度分布调整兴趣度,采用分段函数实现更精确的兴趣度计算 + # 基于实际分布:0-5字符(26.57%), 6-10字符(27.18%), 11-20字符(22.76%), 21-30字符(10.33%), 31+字符(13.86%) + + if text_len == 0: + base_interest = 0.01 # 空消息最低兴趣度 + elif text_len <= 5: + # 1-5字符:线性增长 0.01 -> 0.03 + base_interest = 0.01 + (text_len - 1) * (0.03 - 0.01) / 4 + elif text_len <= 10: + # 6-10字符:线性增长 0.03 -> 0.06 + base_interest = 0.03 + (text_len - 5) * (0.06 - 0.03) / 5 + elif text_len <= 20: + # 11-20字符:线性增长 0.06 -> 0.12 + base_interest = 0.06 + (text_len - 10) * (0.12 - 0.06) / 10 + elif text_len <= 30: + # 21-30字符:线性增长 0.12 -> 0.18 + base_interest = 0.12 + (text_len - 20) * (0.18 - 0.12) / 10 + elif text_len <= 50: + # 31-50字符:线性增长 0.18 -> 0.22 + base_interest = 0.18 + (text_len - 30) * (0.22 - 0.18) / 20 + elif text_len <= 100: + # 51-100字符:线性增长 0.22 -> 0.26 + base_interest = 0.22 + (text_len - 50) * (0.26 - 0.22) / 50 + else: + # 100+字符:对数增长 0.26 -> 0.3,增长率递减 + base_interest = 0.26 + (0.3 - 0.26) * (math.log10(text_len - 99) / math.log10(901)) # 1000-99=901 + + # 确保在范围内 + base_interest = min(max(base_interest, 0.01), 0.3) interested_rate += base_interest diff --git a/src/chat/memory_system/Hippocampus.py b/src/chat/memory_system/Hippocampus.py index ad038416..c1cf6179 100644 --- a/src/chat/memory_system/Hippocampus.py +++ b/src/chat/memory_system/Hippocampus.py @@ -299,6 +299,63 @@ class Hippocampus: # 按相似度降序排序 memories.sort(key=lambda x: x[2], reverse=True) return memories + + async def get_keywords_from_text(self, text: str, fast_retrieval: bool = False) -> list: + """从文本中提取关键词。 + + Args: + text (str): 输入文本 + fast_retrieval (bool, optional): 是否使用快速检索。默认为False。 + 如果为True,使用jieba分词提取关键词,速度更快但可能不够准确。 + 如果为False,使用LLM提取关键词,速度较慢但更准确。 + """ + if not text: + return [] + + if fast_retrieval: + # 使用jieba分词提取关键词 + words = jieba.cut(text) + # 过滤掉停用词和单字词 + keywords = [word for word in words if len(word) > 1] + # 去重 + keywords = list(set(keywords)) + # 限制关键词数量 + logger.debug(f"提取关键词: {keywords}") + + else: + # 使用LLM提取关键词 - 根据详细文本长度分布优化topic_num计算 + text_length = len(text) + if text_length <= 5: + topic_num = 1 # 1-5字符: 1个关键词 (26.57%的文本) + elif text_length <= 10: + topic_num = 1 # 6-10字符: 1个关键词 (27.18%的文本) + elif text_length <= 20: + topic_num = 2 # 11-20字符: 2个关键词 (22.76%的文本) + elif text_length <= 30: + topic_num = 3 # 21-30字符: 3个关键词 (10.33%的文本) + elif text_length <= 50: + topic_num = 4 # 31-50字符: 4个关键词 (9.79%的文本) + else: + topic_num = 5 # 51+字符: 5个关键词 (其余长文本) + + # logger.info(f"提取关键词数量: {topic_num}") + topics_response, (reasoning_content, model_name) = await self.model_summary.generate_response_async( + self.find_topic_llm(text, topic_num) + ) + + # 提取关键词 + keywords = re.findall(r"<([^>]+)>", topics_response) + if not keywords: + keywords = [] + else: + keywords = [ + keyword.strip() + for keyword in ",".join(keywords).replace(",", ",").replace("、", ",").replace(" ", ",").split(",") + if keyword.strip() + ] + + return keywords + async def get_memory_from_text( self, @@ -325,39 +382,7 @@ class Hippocampus: - memory_items: list, 该主题下的记忆项列表 - similarity: float, 与文本的相似度 """ - if not text: - return [] - - if fast_retrieval: - # 使用jieba分词提取关键词 - words = jieba.cut(text) - # 过滤掉停用词和单字词 - keywords = [word for word in words if len(word) > 1] - # 去重 - keywords = list(set(keywords)) - # 限制关键词数量 - logger.debug(f"提取关键词: {keywords}") - - else: - # 使用LLM提取关键词 - topic_num = min(5, max(1, int(len(text) * 0.1))) # 根据文本长度动态调整关键词数量 - # logger.info(f"提取关键词数量: {topic_num}") - topics_response, (reasoning_content, model_name) = await self.model_summary.generate_response_async( - self.find_topic_llm(text, topic_num) - ) - - # 提取关键词 - keywords = re.findall(r"<([^>]+)>", topics_response) - if not keywords: - keywords = [] - else: - keywords = [ - keyword.strip() - for keyword in ",".join(keywords).replace(",", ",").replace("、", ",").replace(" ", ",").split(",") - if keyword.strip() - ] - - # logger.info(f"提取的关键词: {', '.join(keywords)}") + keywords = await self.get_keywords_from_text(text, fast_retrieval) # 过滤掉不存在于记忆图中的关键词 valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] @@ -679,38 +704,7 @@ class Hippocampus: Returns: float: 激活节点数与总节点数的比值 """ - if not text: - return 0 - - if fast_retrieval: - # 使用jieba分词提取关键词 - words = jieba.cut(text) - # 过滤掉停用词和单字词 - keywords = [word for word in words if len(word) > 1] - # 去重 - keywords = list(set(keywords)) - # 限制关键词数量 - keywords = keywords[:5] - else: - # 使用LLM提取关键词 - topic_num = min(5, max(1, int(len(text) * 0.1))) # 根据文本长度动态调整关键词数量 - # logger.info(f"提取关键词数量: {topic_num}") - topics_response, (reasoning_content, model_name) = await self.model_summary.generate_response_async( - self.find_topic_llm(text, topic_num) - ) - - # 提取关键词 - keywords = re.findall(r"<([^>]+)>", topics_response) - if not keywords: - keywords = [] - else: - keywords = [ - keyword.strip() - for keyword in ",".join(keywords).replace(",", ",").replace("、", ",").replace(" ", ",").split(",") - if keyword.strip() - ] - - # logger.info(f"提取的关键词: {', '.join(keywords)}") + keywords = await self.get_keywords_from_text(text, fast_retrieval) # 过滤掉不存在于记忆图中的关键词 valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] diff --git a/src/mais4u/mais4u_chat/s4u_msg_processor.py b/src/mais4u/mais4u_chat/s4u_msg_processor.py index cbc7d3fa..c5ad9ca1 100644 --- a/src/mais4u/mais4u_chat/s4u_msg_processor.py +++ b/src/mais4u/mais4u_chat/s4u_msg_processor.py @@ -47,11 +47,35 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool]: logger.debug(f"记忆激活率: {interested_rate:.2f}") text_len = len(message.processed_plain_text) - # 根据文本长度调整兴趣度,长度越大兴趣度越高,但增长率递减,最低0.01,最高0.05 - # 采用对数函数实现递减增长 - - base_interest = 0.01 + (0.05 - 0.01) * (math.log10(text_len + 1) / math.log10(1000 + 1)) - base_interest = min(max(base_interest, 0.01), 0.05) + # 根据文本长度分布调整兴趣度,采用分段函数实现更精确的兴趣度计算 + # 基于实际分布:0-5字符(26.57%), 6-10字符(27.18%), 11-20字符(22.76%), 21-30字符(10.33%), 31+字符(13.86%) + + if text_len == 0: + base_interest = 0.01 # 空消息最低兴趣度 + elif text_len <= 5: + # 1-5字符:线性增长 0.01 -> 0.03 + base_interest = 0.01 + (text_len - 1) * (0.03 - 0.01) / 4 + elif text_len <= 10: + # 6-10字符:线性增长 0.03 -> 0.06 + base_interest = 0.03 + (text_len - 5) * (0.06 - 0.03) / 5 + elif text_len <= 20: + # 11-20字符:线性增长 0.06 -> 0.12 + base_interest = 0.06 + (text_len - 10) * (0.12 - 0.06) / 10 + elif text_len <= 30: + # 21-30字符:线性增长 0.12 -> 0.18 + base_interest = 0.12 + (text_len - 20) * (0.18 - 0.12) / 10 + elif text_len <= 50: + # 31-50字符:线性增长 0.18 -> 0.22 + base_interest = 0.18 + (text_len - 30) * (0.22 - 0.18) / 20 + elif text_len <= 100: + # 51-100字符:线性增长 0.22 -> 0.26 + base_interest = 0.22 + (text_len - 50) * (0.26 - 0.22) / 50 + else: + # 100+字符:对数增长 0.26 -> 0.3,增长率递减 + base_interest = 0.26 + (0.3 - 0.26) * (math.log10(text_len - 99) / math.log10(901)) # 1000-99=901 + + # 确保在范围内 + base_interest = min(max(base_interest, 0.01), 0.3) interested_rate += base_interest From 5c5d23c218e2d4683843d82b2e415342619b97fb Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Fri, 25 Jul 2025 15:58:01 +0800 Subject: [PATCH 20/26] =?UTF-8?q?=E6=94=B9=E6=88=90=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E9=98=B2=E6=AD=A2=E4=B9=B1=E4=B8=83=E5=85=AB?= =?UTF-8?q?=E7=B3=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/built_in/plugin_management/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/built_in/plugin_management/plugin.py b/src/plugins/built_in/plugin_management/plugin.py index cbdf567a..76f1a68b 100644 --- a/src/plugins/built_in/plugin_management/plugin.py +++ b/src/plugins/built_in/plugin_management/plugin.py @@ -422,13 +422,13 @@ class ManagementCommand(BaseCommand): @register_plugin class PluginManagementPlugin(BasePlugin): plugin_name: str = "plugin_management_plugin" - enable_plugin: bool = True + enable_plugin: bool = False dependencies: list[str] = [] python_dependencies: list[str] = [] config_file_name: str = "config.toml" config_schema: dict = { "plugin": { - "enable": ConfigField(bool, default=True, description="是否启用插件"), + "enable": ConfigField(bool, default=False, description="是否启用插件"), "permission": ConfigField(list, default=[], description="有权限使用插件管理命令的用户列表"), }, } From a725f70ee2785305128c0bad98947e79f5b75be3 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Fri, 25 Jul 2025 16:06:39 +0800 Subject: [PATCH 21/26] issue fix --- src/plugins/built_in/core_actions/plugin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/plugins/built_in/core_actions/plugin.py b/src/plugins/built_in/core_actions/plugin.py index 99bff18a..c34f5a87 100644 --- a/src/plugins/built_in/core_actions/plugin.py +++ b/src/plugins/built_in/core_actions/plugin.py @@ -66,13 +66,12 @@ class CoreActionsPlugin(BasePlugin): if global_config.emoji.emoji_activate_type == "llm": EmojiAction.random_activation_probability = 0.0 - EmojiAction.focus_activation_type = ActionActivationType.LLM_JUDGE - EmojiAction.normal_activation_type = ActionActivationType.LLM_JUDGE + EmojiAction.activation_type = ActionActivationType.LLM_JUDGE elif global_config.emoji.emoji_activate_type == "random": EmojiAction.random_activation_probability = global_config.emoji.emoji_chance - EmojiAction.focus_activation_type = ActionActivationType.RANDOM - EmojiAction.normal_activation_type = ActionActivationType.RANDOM + EmojiAction.activation_type = ActionActivationType.RANDOM + # --- 根据配置注册组件 --- components = [] if self.get_config("components.enable_reply", True): From c53dc6cb69f250deb1663fa17010a2cb8c4a09a1 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Jul 2025 16:14:41 +0800 Subject: [PATCH 22/26] =?UTF-8?q?better=EF=BC=9A=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E6=BF=80=E6=B4=BB=E6=B7=B1=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../heart_flow/heartflow_message_processor.py | 1 + src/chat/memory_system/Hippocampus.py | 20 ++++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/chat/heart_flow/heartflow_message_processor.py b/src/chat/heart_flow/heartflow_message_processor.py index 57b52ae6..6956dbda 100644 --- a/src/chat/heart_flow/heartflow_message_processor.py +++ b/src/chat/heart_flow/heartflow_message_processor.py @@ -56,6 +56,7 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool]: with Timer("记忆激活"): interested_rate = await hippocampus_manager.get_activate_from_text( message.processed_plain_text, + max_depth= 5, fast_retrieval=False, ) logger.debug(f"记忆激活率: {interested_rate:.2f}") diff --git a/src/chat/memory_system/Hippocampus.py b/src/chat/memory_system/Hippocampus.py index c1cf6179..c2dfc218 100644 --- a/src/chat/memory_system/Hippocampus.py +++ b/src/chat/memory_system/Hippocampus.py @@ -224,10 +224,15 @@ class Hippocampus: return hash((source, target)) @staticmethod - def find_topic_llm(text, topic_num): - # sourcery skip: inline-immediately-returned-variable + def find_topic_llm(text:str, topic_num:int|list[int]): + topic_num_str = "" + if isinstance(topic_num, list): + topic_num_str = f"{topic_num[0]}-{topic_num[1]}" + else: + topic_num_str = topic_num + prompt = ( - f"这是一段文字:\n{text}\n\n请你从这段话中总结出最多{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来," + f"这是一段文字:\n{text}\n\n请你从这段话中总结出最多{topic_num_str}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来," f"将主题用逗号隔开,并加上<>,例如<主题1>,<主题2>......尽可能精简。只需要列举最多{topic_num}个话题就好,不要有序号,不要告诉我其他内容。" f"如果确定找不出主题或者没有明显主题,返回。" ) @@ -325,12 +330,13 @@ class Hippocampus: else: # 使用LLM提取关键词 - 根据详细文本长度分布优化topic_num计算 text_length = len(text) + topic_num:str|list[int] = None if text_length <= 5: - topic_num = 1 # 1-5字符: 1个关键词 (26.57%的文本) + topic_num = [1,2] # 1-5字符: 1个关键词 (26.57%的文本) elif text_length <= 10: - topic_num = 1 # 6-10字符: 1个关键词 (27.18%的文本) + topic_num = 2 # 6-10字符: 1个关键词 (27.18%的文本) elif text_length <= 20: - topic_num = 2 # 11-20字符: 2个关键词 (22.76%的文本) + topic_num = [2,3] # 11-20字符: 2个关键词 (22.76%的文本) elif text_length <= 30: topic_num = 3 # 21-30字符: 3个关键词 (10.33%的文本) elif text_length <= 50: @@ -721,7 +727,7 @@ class Hippocampus: for keyword in valid_keywords: logger.debug(f"开始以关键词 '{keyword}' 为中心进行扩散检索 (最大深度: {max_depth}):") # 初始化激活值 - activation_values = {keyword: 1.0} + activation_values = {keyword: 1.5} # 记录已访问的节点 visited_nodes = {keyword} # 待处理的节点队列,每个元素是(节点, 激活值, 当前深度) From 6900a8b26959ee33cabf80558ed4b5a2dd7fd66b Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Jul 2025 16:51:13 +0800 Subject: [PATCH 23/26] =?UTF-8?q?feat=EF=BC=9A=E4=BC=98=E5=8C=96=E5=85=B3?= =?UTF-8?q?=E9=94=AE=E8=AF=8D=E6=8F=90=E5=8F=96=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?at=E5=92=8C=E5=9B=9E=E5=A4=8D=E7=9A=84=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../heart_flow/heartflow_message_processor.py | 9 + src/chat/memory_system/Hippocampus.py | 81 +++---- src/chat/replyer/default_generator.py | 10 +- src/chat/utils/chat_message_builder.py | 216 +++++++++++++----- src/plugin_system/apis/send_api.py | 31 +-- 5 files changed, 218 insertions(+), 129 deletions(-) diff --git a/src/chat/heart_flow/heartflow_message_processor.py b/src/chat/heart_flow/heartflow_message_processor.py index 6956dbda..95b05989 100644 --- a/src/chat/heart_flow/heartflow_message_processor.py +++ b/src/chat/heart_flow/heartflow_message_processor.py @@ -12,6 +12,7 @@ from src.chat.message_receive.storage import MessageStorage from src.chat.heart_flow.heartflow import heartflow from src.chat.utils.utils import is_mentioned_bot_in_message from src.chat.utils.timer_calculator import Timer +from src.chat.utils.chat_message_builder import replace_user_references_in_content 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 @@ -148,6 +149,14 @@ class HeartFCMessageReceiver: # 如果消息中包含图片标识,则将 [picid:...] 替换为 [图片] picid_pattern = r"\[picid:([^\]]+)\]" processed_plain_text = re.sub(picid_pattern, "[图片]", message.processed_plain_text) + + # 应用用户引用格式替换,将回复和@格式转换为可读格式 + processed_plain_text = replace_user_references_in_content( + processed_plain_text, + message.message_info.platform, + is_async=False, + replace_bot_name=True + ) logger.info(f"[{mes_name}]{userinfo.user_nickname}:{processed_plain_text}[兴趣度:{interested_rate:.2f}]") # type: ignore diff --git a/src/chat/memory_system/Hippocampus.py b/src/chat/memory_system/Hippocampus.py index c2dfc218..13cf53f2 100644 --- a/src/chat/memory_system/Hippocampus.py +++ b/src/chat/memory_system/Hippocampus.py @@ -305,7 +305,7 @@ class Hippocampus: memories.sort(key=lambda x: x[2], reverse=True) return memories - async def get_keywords_from_text(self, text: str, fast_retrieval: bool = False) -> list: + async def get_keywords_from_text(self, text: str) -> list: """从文本中提取关键词。 Args: @@ -317,50 +317,45 @@ class Hippocampus: if not text: return [] - if fast_retrieval: - # 使用jieba分词提取关键词 + # 使用LLM提取关键词 - 根据详细文本长度分布优化topic_num计算 + text_length = len(text) + topic_num:str|list[int] = None + if text_length <= 5: words = jieba.cut(text) - # 过滤掉停用词和单字词 keywords = [word for word in words if len(word) > 1] - # 去重 - keywords = list(set(keywords)) - # 限制关键词数量 - logger.debug(f"提取关键词: {keywords}") - + keywords = list(set(keywords))[:3] # 限制最多3个关键词 + logger.info(f"提取关键词: {keywords}") + return keywords + elif text_length <= 10: + topic_num = [1,3] # 6-10字符: 1个关键词 (27.18%的文本) + elif text_length <= 20: + topic_num = [2,4] # 11-20字符: 2个关键词 (22.76%的文本) + elif text_length <= 30: + topic_num = [3,5] # 21-30字符: 3个关键词 (10.33%的文本) + elif text_length <= 50: + topic_num = [4,5] # 31-50字符: 4个关键词 (9.79%的文本) else: - # 使用LLM提取关键词 - 根据详细文本长度分布优化topic_num计算 - text_length = len(text) - topic_num:str|list[int] = None - if text_length <= 5: - topic_num = [1,2] # 1-5字符: 1个关键词 (26.57%的文本) - elif text_length <= 10: - topic_num = 2 # 6-10字符: 1个关键词 (27.18%的文本) - elif text_length <= 20: - topic_num = [2,3] # 11-20字符: 2个关键词 (22.76%的文本) - elif text_length <= 30: - topic_num = 3 # 21-30字符: 3个关键词 (10.33%的文本) - elif text_length <= 50: - topic_num = 4 # 31-50字符: 4个关键词 (9.79%的文本) - else: - topic_num = 5 # 51+字符: 5个关键词 (其余长文本) - - # logger.info(f"提取关键词数量: {topic_num}") - topics_response, (reasoning_content, model_name) = await self.model_summary.generate_response_async( - self.find_topic_llm(text, topic_num) - ) + topic_num = 5 # 51+字符: 5个关键词 (其余长文本) + + + topics_response, (reasoning_content, model_name) = await self.model_summary.generate_response_async( + self.find_topic_llm(text, topic_num) + ) - # 提取关键词 - keywords = re.findall(r"<([^>]+)>", topics_response) - if not keywords: - keywords = [] - else: - keywords = [ - keyword.strip() - for keyword in ",".join(keywords).replace(",", ",").replace("、", ",").replace(" ", ",").split(",") - if keyword.strip() - ] - - return keywords + # 提取关键词 + keywords = re.findall(r"<([^>]+)>", topics_response) + if not keywords: + keywords = [] + else: + keywords = [ + keyword.strip() + for keyword in ",".join(keywords).replace(",", ",").replace("、", ",").replace(" ", ",").split(",") + if keyword.strip() + ] + + logger.info(f"提取关键词: {keywords}") + + return keywords async def get_memory_from_text( @@ -388,7 +383,7 @@ class Hippocampus: - memory_items: list, 该主题下的记忆项列表 - similarity: float, 与文本的相似度 """ - keywords = await self.get_keywords_from_text(text, fast_retrieval) + keywords = await self.get_keywords_from_text(text) # 过滤掉不存在于记忆图中的关键词 valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] @@ -710,7 +705,7 @@ class Hippocampus: Returns: float: 激活节点数与总节点数的比值 """ - keywords = await self.get_keywords_from_text(text, fast_retrieval) + keywords = await self.get_keywords_from_text(text) # 过滤掉不存在于记忆图中的关键词 valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 9d75671c..efefa093 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -17,7 +17,7 @@ from src.chat.message_receive.uni_message_sender import HeartFCSender from src.chat.utils.timer_calculator import Timer # <--- Import Timer from src.chat.utils.utils import get_chat_type_and_target_info from src.chat.utils.prompt_builder import Prompt, global_prompt_manager -from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat +from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat, replace_user_references_in_content from src.chat.express.expression_selector import expression_selector from src.chat.knowledge.knowledge_lib import qa_manager from src.chat.memory_system.memory_activator import MemoryActivator @@ -629,6 +629,14 @@ class DefaultReplyer: mood_prompt = "" sender, target = self._parse_reply_target(reply_to) + + target = replace_user_references_in_content( + target, + chat_stream.platform, + is_async=False, + replace_bot_name=True + ) + # 构建action描述 (如果启用planner) action_descriptions = "" diff --git a/src/chat/utils/chat_message_builder.py b/src/chat/utils/chat_message_builder.py index 3a08ca72..22f56d1d 100644 --- a/src/chat/utils/chat_message_builder.py +++ b/src/chat/utils/chat_message_builder.py @@ -2,7 +2,7 @@ import time # 导入 time 模块以获取当前时间 import random import re -from typing import List, Dict, Any, Tuple, Optional +from typing import List, Dict, Any, Tuple, Optional, Union, Callable from rich.traceback import install from src.config.config import global_config @@ -15,6 +15,155 @@ from src.chat.utils.utils import translate_timestamp_to_human_readable,assign_me install(extra_lines=3) +def replace_user_references_in_content( + content: str, + platform: str, + name_resolver: Union[Callable[[str, str], str], Callable[[str, str], Any]] = None, + is_async: bool = False, + replace_bot_name: bool = True +) -> Union[str, Any]: + """ + 替换内容中的用户引用格式,包括回复和@格式 + + Args: + content: 要处理的内容字符串 + platform: 平台标识 + name_resolver: 名称解析函数,接收(platform, user_id)参数,返回用户名称 + 如果为None,则使用默认的person_info_manager + is_async: 是否为异步模式 + replace_bot_name: 是否将机器人的user_id替换为"机器人昵称(你)" + + Returns: + 处理后的内容字符串(同步模式)或awaitable对象(异步模式) + """ + if is_async: + return _replace_user_references_async(content, platform, name_resolver, replace_bot_name) + else: + return _replace_user_references_sync(content, platform, name_resolver, replace_bot_name) + + +def _replace_user_references_sync( + content: str, + platform: str, + name_resolver: Optional[Callable[[str, str], str]] = None, + replace_bot_name: bool = True +) -> str: + """同步版本的用户引用替换""" + if name_resolver is None: + person_info_manager = get_person_info_manager() + def default_resolver(platform: str, user_id: str) -> str: + # 检查是否是机器人自己 + if replace_bot_name and user_id == global_config.bot.qq_account: + return f"{global_config.bot.nickname}(你)" + person_id = PersonInfoManager.get_person_id(platform, user_id) + return person_info_manager.get_value_sync(person_id, "person_name") or user_id + name_resolver = default_resolver + + # 处理回复格式 + reply_pattern = r"回复<([^:<>]+):([^:<>]+)>" + match = re.search(reply_pattern, content) + if match: + aaa = match.group(1) + bbb = match.group(2) + try: + # 检查是否是机器人自己 + if replace_bot_name and bbb == global_config.bot.qq_account: + reply_person_name = f"{global_config.bot.nickname}(你)" + else: + reply_person_name = name_resolver(platform, bbb) or aaa + content = re.sub(reply_pattern, f"回复 {reply_person_name}", content, count=1) + except Exception: + # 如果解析失败,使用原始昵称 + content = re.sub(reply_pattern, f"回复 {aaa}", content, count=1) + + # 处理@格式 + at_pattern = r"@<([^:<>]+):([^:<>]+)>" + at_matches = list(re.finditer(at_pattern, content)) + if at_matches: + new_content = "" + last_end = 0 + for m in at_matches: + new_content += content[last_end:m.start()] + aaa = m.group(1) + bbb = m.group(2) + try: + # 检查是否是机器人自己 + if replace_bot_name and bbb == global_config.bot.qq_account: + at_person_name = f"{global_config.bot.nickname}(你)" + else: + at_person_name = name_resolver(platform, bbb) or aaa + new_content += f"@{at_person_name}" + except Exception: + # 如果解析失败,使用原始昵称 + new_content += f"@{aaa}" + last_end = m.end() + new_content += content[last_end:] + content = new_content + + return content + + +async def _replace_user_references_async( + content: str, + platform: str, + name_resolver: Optional[Callable[[str, str], Any]] = None, + replace_bot_name: bool = True +) -> str: + """异步版本的用户引用替换""" + if name_resolver is None: + person_info_manager = get_person_info_manager() + async def default_resolver(platform: str, user_id: str) -> str: + # 检查是否是机器人自己 + if replace_bot_name and user_id == global_config.bot.qq_account: + return f"{global_config.bot.nickname}(你)" + person_id = PersonInfoManager.get_person_id(platform, user_id) + return await person_info_manager.get_value(person_id, "person_name") or user_id + name_resolver = default_resolver + + # 处理回复格式 + reply_pattern = r"回复<([^:<>]+):([^:<>]+)>" + match = re.search(reply_pattern, content) + if match: + aaa = match.group(1) + bbb = match.group(2) + try: + # 检查是否是机器人自己 + if replace_bot_name and bbb == global_config.bot.qq_account: + reply_person_name = f"{global_config.bot.nickname}(你)" + else: + reply_person_name = await name_resolver(platform, bbb) or aaa + content = re.sub(reply_pattern, f"回复 {reply_person_name}", content, count=1) + except Exception: + # 如果解析失败,使用原始昵称 + content = re.sub(reply_pattern, f"回复 {aaa}", content, count=1) + + # 处理@格式 + at_pattern = r"@<([^:<>]+):([^:<>]+)>" + at_matches = list(re.finditer(at_pattern, content)) + if at_matches: + new_content = "" + last_end = 0 + for m in at_matches: + new_content += content[last_end:m.start()] + aaa = m.group(1) + bbb = m.group(2) + try: + # 检查是否是机器人自己 + if replace_bot_name and bbb == global_config.bot.qq_account: + at_person_name = f"{global_config.bot.nickname}(你)" + else: + at_person_name = await name_resolver(platform, bbb) or aaa + new_content += f"@{at_person_name}" + except Exception: + # 如果解析失败,使用原始昵称 + new_content += f"@{aaa}" + last_end = m.end() + new_content += content[last_end:] + content = new_content + + return content + + def get_raw_msg_by_timestamp( timestamp_start: float, timestamp_end: float, limit: int = 0, limit_mode: str = "latest" ) -> List[Dict[str, Any]]: @@ -374,33 +523,8 @@ def _build_readable_messages_internal( else: person_name = "某人" - # 检查是否有 回复 字段 - reply_pattern = r"回复<([^:<>]+):([^:<>]+)>" - match = re.search(reply_pattern, content) - if match: - aaa: str = match[1] - bbb: str = match[2] - reply_person_id = PersonInfoManager.get_person_id(platform, bbb) - reply_person_name = person_info_manager.get_value_sync(reply_person_id, "person_name") or aaa - # 在内容前加上回复信息 - content = re.sub(reply_pattern, lambda m, name=reply_person_name: f"回复 {name}", content, count=1) - - # 检查是否有 @ 字段 @<{member_info.get('nickname')}:{member_info.get('user_id')}> - at_pattern = r"@<([^:<>]+):([^:<>]+)>" - at_matches = list(re.finditer(at_pattern, content)) - if at_matches: - new_content = "" - last_end = 0 - for m in at_matches: - new_content += content[last_end : m.start()] - aaa = m.group(1) - bbb = m.group(2) - at_person_id = PersonInfoManager.get_person_id(platform, bbb) - at_person_name = person_info_manager.get_value_sync(at_person_id, "person_name") or aaa - new_content += f"@{at_person_name}" - last_end = m.end() - new_content += content[last_end:] - content = new_content + # 使用独立函数处理用户引用格式 + content = replace_user_references_in_content(content, platform, is_async=False, replace_bot_name=replace_bot_name) target_str = "这是QQ的一个功能,用于提及某人,但没那么明显" if target_str in content and random.random() < 0.6: @@ -916,38 +1040,14 @@ async def build_anonymous_messages(messages: List[Dict[str, Any]]) -> str: anon_name = get_anon_name(platform, user_id) # print(f"anon_name:{anon_name}") - # 处理 回复 - reply_pattern = r"回复<([^:<>]+):([^:<>]+)>" - match = re.search(reply_pattern, content) - if match: - # print(f"发现回复match:{match}") - bbb = match.group(2) + # 使用独立函数处理用户引用格式,传入自定义的匿名名称解析器 + def anon_name_resolver(platform: str, user_id: str) -> str: try: - anon_reply = get_anon_name(platform, bbb) - # print(f"anon_reply:{anon_reply}") + return get_anon_name(platform, user_id) except Exception: - anon_reply = "?" - content = re.sub(reply_pattern, f"回复 {anon_reply}", content, count=1) - - # 处理 @,无嵌套def - at_pattern = r"@<([^:<>]+):([^:<>]+)>" - at_matches = list(re.finditer(at_pattern, content)) - if at_matches: - # print(f"发现@match:{at_matches}") - new_content = "" - last_end = 0 - for m in at_matches: - new_content += content[last_end : m.start()] - bbb = m.group(2) - try: - anon_at = get_anon_name(platform, bbb) - # print(f"anon_at:{anon_at}") - except Exception: - anon_at = "?" - new_content += f"@{anon_at}" - last_end = m.end() - new_content += content[last_end:] - content = new_content + return "?" + + content = replace_user_references_in_content(content, platform, anon_name_resolver, is_async=False, replace_bot_name=False) header = f"{anon_name}说 " output_lines.append(header) diff --git a/src/plugin_system/apis/send_api.py b/src/plugin_system/apis/send_api.py index 352ccdb4..f7b3092e 100644 --- a/src/plugin_system/apis/send_api.py +++ b/src/plugin_system/apis/send_api.py @@ -19,6 +19,7 @@ await send_api.custom_message("video", video_data, "123456", True) """ +import asyncio import traceback import time import difflib @@ -30,7 +31,7 @@ from src.common.logger import get_logger from src.chat.message_receive.chat_stream import get_chat_manager from src.chat.message_receive.uni_message_sender import HeartFCSender from src.chat.message_receive.message import MessageSending, MessageRecv -from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat +from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat, replace_user_references_in_content from src.person_info.person_info import get_person_info_manager from maim_message import Seg, UserInfo from src.config.config import global_config @@ -183,32 +184,8 @@ async def _find_reply_message(target_stream, reply_to: str) -> Optional[MessageR if person_name == sender: translate_text = message["processed_plain_text"] - # 检查是否有 回复 字段 - reply_pattern = r"回复<([^:<>]+):([^:<>]+)>" - if match := re.search(reply_pattern, translate_text): - aaa = match.group(1) - bbb = match.group(2) - reply_person_id = get_person_info_manager().get_person_id(platform, bbb) - reply_person_name = await get_person_info_manager().get_value(reply_person_id, "person_name") or aaa - # 在内容前加上回复信息 - translate_text = re.sub(reply_pattern, f"回复 {reply_person_name}", translate_text, count=1) - - # 检查是否有 @ 字段 - at_pattern = r"@<([^:<>]+):([^:<>]+)>" - at_matches = list(re.finditer(at_pattern, translate_text)) - if at_matches: - new_content = "" - last_end = 0 - for m in at_matches: - new_content += translate_text[last_end : m.start()] - aaa = m.group(1) - bbb = m.group(2) - at_person_id = get_person_info_manager().get_person_id(platform, bbb) - at_person_name = await get_person_info_manager().get_value(at_person_id, "person_name") or aaa - new_content += f"@{at_person_name}" - last_end = m.end() - new_content += translate_text[last_end:] - translate_text = new_content + # 使用独立函数处理用户引用格式 + translate_text = await replace_user_references_in_content(translate_text, platform, is_async=True) similarity = difflib.SequenceMatcher(None, text, translate_text).ratio() if similarity >= 0.9: From 7d216343bd1b8fc714712dd93201de8804c7dc46 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Jul 2025 16:57:30 +0800 Subject: [PATCH 24/26] Update mood_manager.py --- src/mood/mood_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mood/mood_manager.py b/src/mood/mood_manager.py index 38ed39bc..eae0ea71 100644 --- a/src/mood/mood_manager.py +++ b/src/mood/mood_manager.py @@ -78,7 +78,7 @@ class ChatMood: if interested_rate <= 0: interest_multiplier = 0 else: - interest_multiplier = 3 * math.pow(interested_rate, 0.25) + interest_multiplier = 2 * math.pow(interested_rate, 0.25) logger.debug( f"base_probability: {base_probability}, time_multiplier: {time_multiplier}, interest_multiplier: {interest_multiplier}" From 0a351e70531f254e6da63a33925c3428ac41cae0 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Jul 2025 17:01:59 +0800 Subject: [PATCH 25/26] changelog --- changelogs/changelog.md | 5 +++-- src/chat/chat_loop/heartFC_chat.py | 14 +++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/changelogs/changelog.md b/changelogs/changelog.md index c56426a7..d6743227 100644 --- a/changelogs/changelog.md +++ b/changelogs/changelog.md @@ -5,8 +5,9 @@ - 修复表达方式迁移空目录问题 - 修复reply_to空字段问题 - 将metioned bot 和 at应用到focus prompt中 - - +- 更好的兴趣度计算 +- 修复部分模型由于enable_thinking导致的400问题 +- 优化关键词提取 ## [0.9.0] - 2025-7-25 diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index 41101b2d..ac8c7d2d 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -330,13 +330,13 @@ class HeartFChatting: if self.loop_mode == ChatMode.NORMAL: if action_type == "no_action": - logger.info(f"[{self.log_prefix}] {global_config.bot.nickname} 决定进行回复") + logger.info(f"{self.log_prefix}{global_config.bot.nickname} 决定进行回复") elif is_parallel: logger.info( - f"[{self.log_prefix}] {global_config.bot.nickname} 决定进行回复, 同时执行{action_type}动作" + f"{self.log_prefix}{global_config.bot.nickname} 决定进行回复, 同时执行{action_type}动作" ) else: - logger.info(f"[{self.log_prefix}] {global_config.bot.nickname} 决定执行{action_type}动作") + logger.info(f"{self.log_prefix}{global_config.bot.nickname} 决定执行{action_type}动作") if action_type == "no_action": # 等待回复生成完毕 @@ -351,15 +351,15 @@ class HeartFChatting: # 模型炸了,没有回复内容生成 if not response_set: - logger.warning(f"[{self.log_prefix}] 模型未生成回复内容") + logger.warning(f"{self.log_prefix}模型未生成回复内容") return False elif action_type not in ["no_action"] and not is_parallel: logger.info( - f"[{self.log_prefix}] {global_config.bot.nickname} 原本想要回复:{content},但选择执行{action_type},不发表回复" + f"{self.log_prefix}{global_config.bot.nickname} 原本想要回复:{content},但选择执行{action_type},不发表回复" ) return False - logger.info(f"[{self.log_prefix}] {global_config.bot.nickname} 决定的回复内容: {content}") + logger.info(f"{self.log_prefix}{global_config.bot.nickname} 决定的回复内容: {content}") # 发送回复 (不再需要传入 chat) reply_text = await self._send_response(response_set, reply_to_str, loop_start_time,message_data) @@ -563,7 +563,7 @@ class HeartFChatting: return reply_set except Exception as e: - logger.error(f"[{self.log_prefix}] 回复生成出现错误:{str(e)} {traceback.format_exc()}") + logger.error(f"{self.log_prefix}回复生成出现错误:{str(e)} {traceback.format_exc()}") return None async def _send_response(self, reply_set, reply_to, thinking_start_time, message_data): From 455c249d351268cf6e25b0d7b8c3c03fd8caed98 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Jul 2025 17:02:52 +0800 Subject: [PATCH 26/26] changelog --- changelogs/changelog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelogs/changelog.md b/changelogs/changelog.md index d6743227..1aa33a99 100644 --- a/changelogs/changelog.md +++ b/changelogs/changelog.md @@ -2,6 +2,7 @@ ## [0.9.1] - 2025-7-25 +- 修复reply导致的planner异常空跳 - 修复表达方式迁移空目录问题 - 修复reply_to空字段问题 - 将metioned bot 和 at应用到focus prompt中 @@ -9,7 +10,7 @@ - 修复部分模型由于enable_thinking导致的400问题 - 优化关键词提取 -## [0.9.0] - 2025-7-25 +## [0.9.0] - 2025-7-24 ### 摘要 MaiBot 0.9.0 重磅升级!本版本带来两大核心突破:**全面重构的插件系统**提供更强大的扩展能力和管理功能;**normal和focus模式统一化处理**大幅简化架构并提升性能。同时新增s4u prompt模式优化、语音消息支持、全新情绪系统和mais4u直播互动功能,为MaiBot带来更自然、更智能的交互体验!