From 13bfaf74695f30fabed946859eb9b3e63556fcfc Mon Sep 17 00:00:00 2001 From: xiaoxi68 <3520824673@qq.com> Date: Sat, 7 Feb 2026 03:02:09 +0800 Subject: [PATCH 1/8] fix(brain): skip planner when no new messages --- src/chat/brain_chat/brain_chat.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/chat/brain_chat/brain_chat.py b/src/chat/brain_chat/brain_chat.py index 8fd2de94..87f05066 100644 --- a/src/chat/brain_chat/brain_chat.py +++ b/src/chat/brain_chat/brain_chat.py @@ -162,7 +162,6 @@ class BrainChatting: ) async def _loopbody(self): # sourcery skip: hoist-if-from-if - # 获取最新消息(用于上下文,但不影响是否调用 observe) recent_messages_list = message_api.get_messages_by_time_in_chat( chat_id=self.stream_id, start_time=self.last_read_time, @@ -174,20 +173,20 @@ class BrainChatting: filter_intercept_message_level=1, ) - # 如果有新消息,更新 last_read_time 并触发事件以打断正在进行的 wait - if len(recent_messages_list) >= 1: - self.last_read_time = time.time() - self._new_message_event.set() # 触发新消息事件,打断 wait + # 没有新增用户消息时,直接等待下一轮,避免对旧上下文反复规划。 + if not recent_messages_list: + await asyncio.sleep(0.2) + return True + + self.last_read_time = time.time() + self._new_message_event.set() # 触发新消息事件,打断正在进行的 wait - # 总是执行一次思考迭代(不管有没有新消息) - # wait 动作会在其内部等待,不需要在这里处理 should_continue = await self._observe(recent_messages_list=recent_messages_list) if not should_continue: # 选择了 complete_talk,返回 False 表示需要等待新消息 return False - # 继续下一次迭代(除非选择了 complete_talk) # 短暂等待后再继续,避免过于频繁的循环 await asyncio.sleep(0.1) From 6079898a0ee691ba454596e6402f50d6e79fec95 Mon Sep 17 00:00:00 2001 From: xiaoxi68 <3520824673@qq.com> Date: Sat, 7 Feb 2026 03:04:20 +0800 Subject: [PATCH 2/8] fix(brain): dedupe side-effect actions per message --- src/chat/brain_chat/brain_chat.py | 58 +++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/chat/brain_chat/brain_chat.py b/src/chat/brain_chat/brain_chat.py index 87f05066..7fcdb01d 100644 --- a/src/chat/brain_chat/brain_chat.py +++ b/src/chat/brain_chat/brain_chat.py @@ -1,4 +1,5 @@ import asyncio +import json import time import traceback import random @@ -101,6 +102,9 @@ class BrainChatting: # 最近一次是否成功进行了 reply,用于选择 BrainPlanner 的 Prompt self._last_successful_reply: bool = False + # side-effect 动作幂等缓存,避免同一触发消息在短时间内重复执行。 + self._recent_side_effect_actions: Dict[str, float] = {} + async def start(self): """检查是否需要启动主循环,如果未激活则启动。""" @@ -161,6 +165,41 @@ class BrainChatting: + (f"\n详情: {'; '.join(timer_strings)}" if timer_strings else "") ) + def _is_side_effect_action(self, action_type: str) -> bool: + non_side_effect_actions = {"reply", "wait", "wait_time", "listening", "complete_talk", "no_reply"} + return action_type not in non_side_effect_actions + + def _build_side_effect_action_key(self, action_planner_info: ActionPlannerInfo) -> str: + action_data = dict(action_planner_info.action_data or {}) + action_data.pop("loop_start_time", None) + + target_message = action_planner_info.action_message + target_message_id = "" + if target_message is not None: + target_message_id = str(getattr(target_message, "message_id", "") or "") + + payload = { + "action_type": action_planner_info.action_type, + "target_message_id": target_message_id, + "action_data": action_data, + } + return json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str) + + def _cleanup_recent_side_effect_actions(self, now: float) -> None: + dedupe_window_sec = 120.0 + expired_keys = [ + key + for key, ts in self._recent_side_effect_actions.items() + if now - ts > dedupe_window_sec + ] + for key in expired_keys: + del self._recent_side_effect_actions[key] + + def _is_duplicate_side_effect_action(self, key: str, now: float) -> bool: + dedupe_window_sec = 120.0 + last_ts = self._recent_side_effect_actions.get(key) + return last_ts is not None and now - last_ts <= dedupe_window_sec + async def _loopbody(self): # sourcery skip: hoist-if-from-if recent_messages_list = message_api.get_messages_by_time_in_chat( chat_id=self.stream_id, @@ -580,6 +619,22 @@ class BrainChatting: """执行单个动作的通用函数""" try: with Timer(f"动作{action_planner_info.action_type}", cycle_timers): + side_effect_action_key = "" + if self._is_side_effect_action(action_planner_info.action_type): + side_effect_action_key = self._build_side_effect_action_key(action_planner_info) + now = time.time() + self._cleanup_recent_side_effect_actions(now) + if self._is_duplicate_side_effect_action(side_effect_action_key, now): + logger.info( + f"{self.log_prefix} 跳过重复副作用动作: {action_planner_info.action_type}" + ) + return { + "action_type": action_planner_info.action_type, + "success": True, + "reply_text": "", + "command": "", + } + if action_planner_info.action_type == "complete_talk": # 直接处理complete_talk逻辑,不再通过动作系统 reason = action_planner_info.reasoning or "选择完成对话" @@ -783,6 +838,9 @@ class BrainChatting: if success and action_planner_info.action_type != "reply": self._last_successful_reply = False + if success and side_effect_action_key: + self._recent_side_effect_actions[side_effect_action_key] = time.time() + return { "action_type": action_planner_info.action_type, "success": success, From 32bb6950942b0bfb046f003545cef023d900649a Mon Sep 17 00:00:00 2001 From: xiaoxi68 <3520824673@qq.com> Date: Sat, 7 Feb 2026 03:06:07 +0800 Subject: [PATCH 3/8] fix(planner): block side-effect actions without new user input --- src/chat/brain_chat/brain_planner.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/chat/brain_chat/brain_planner.py b/src/chat/brain_chat/brain_planner.py index 7d5990e4..f5e846c0 100644 --- a/src/chat/brain_chat/brain_planner.py +++ b/src/chat/brain_chat/brain_planner.py @@ -20,7 +20,7 @@ from src.chat.utils.chat_message_builder import ( build_readable_messages_with_id, get_raw_msg_before_timestamp_with_chat, ) -from src.chat.utils.utils import get_chat_type_and_target_info +from src.chat.utils.utils import get_chat_type_and_target_info, is_bot_self from src.chat.planner_actions.action_manager import ActionManager from src.chat.message_receive.chat_stream import get_chat_manager from src.plugin_system.base.component_types import ActionInfo, ComponentType, ActionActivationType @@ -280,6 +280,8 @@ class BrainPlanner: show_actions=True, ) + previous_obs_time_mark = self.last_obs_time_mark + message_list_before_now_short = message_list_before_now[-int(global_config.chat.max_context_size * 0.3) :] chat_content_block_short, message_id_list_short = build_readable_messages_with_id( messages=message_list_before_now_short, @@ -322,6 +324,21 @@ class BrainPlanner: loop_start_time=loop_start_time, ) + has_new_user_message = any( + (msg.time or 0.0) > previous_obs_time_mark + and not is_bot_self(msg.user_info.platform or "", str(msg.user_info.user_id)) + for msg in message_list_before_now + ) + if not has_new_user_message: + non_side_effect_actions = {"reply", "wait", "wait_time", "listening", "complete_talk", "no_reply"} + side_effect_actions = [a.action_type for a in actions if a.action_type not in non_side_effect_actions] + if side_effect_actions: + logger.info( + f"{self.log_prefix}检测到无新用户消息,跳过副作用动作: {' '.join(side_effect_actions)}" + ) + actions = self._create_complete_talk("没有新的用户消息,跳过副作用动作", available_actions) + reasoning = f"{reasoning};检测到无新用户消息,已跳过副作用动作" + # 记录和展示计划日志 logger.info( f"{self.log_prefix}Planner: {reasoning}。选择了{len(actions)}个动作: {' '.join([a.action_type for a in actions])}" From bdd41a0c4743c6f3ef680ec6b85dcfd41553a2c4 Mon Sep 17 00:00:00 2001 From: xiaoxi68 <3520824673@qq.com> Date: Sat, 7 Feb 2026 03:07:09 +0800 Subject: [PATCH 4/8] chore(planner): remove duplicate prompt logging --- src/chat/brain_chat/brain_planner.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/chat/brain_chat/brain_planner.py b/src/chat/brain_chat/brain_planner.py index f5e846c0..070a1f40 100644 --- a/src/chat/brain_chat/brain_planner.py +++ b/src/chat/brain_chat/brain_planner.py @@ -534,9 +534,6 @@ class BrainPlanner: llm_duration_ms = (time.perf_counter() - llm_start) * 1000 llm_reasoning = reasoning_content - logger.info(f"{self.log_prefix}规划器原始提示词: {prompt}") - logger.info(f"{self.log_prefix}规划器原始响应: {llm_content}") - if global_config.debug.show_planner_prompt: logger.info(f"{self.log_prefix}规划器原始提示词: {prompt}") logger.info(f"{self.log_prefix}规划器原始响应: {llm_content}") From f7940b03930b4ab239ea0b2d43662a908918a3b3 Mon Sep 17 00:00:00 2001 From: xiaoxi68 <3520824673@qq.com> Date: Sat, 7 Feb 2026 04:03:17 +0800 Subject: [PATCH 5/8] tune(brain): reduce side-effect dedupe window to 20s --- src/chat/brain_chat/brain_chat.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/chat/brain_chat/brain_chat.py b/src/chat/brain_chat/brain_chat.py index 7fcdb01d..8bbf1d59 100644 --- a/src/chat/brain_chat/brain_chat.py +++ b/src/chat/brain_chat/brain_chat.py @@ -104,6 +104,7 @@ class BrainChatting: # side-effect 动作幂等缓存,避免同一触发消息在短时间内重复执行。 self._recent_side_effect_actions: Dict[str, float] = {} + self._side_effect_dedupe_window_sec = 20.0 async def start(self): """检查是否需要启动主循环,如果未激活则启动。""" @@ -186,7 +187,7 @@ class BrainChatting: return json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str) def _cleanup_recent_side_effect_actions(self, now: float) -> None: - dedupe_window_sec = 120.0 + dedupe_window_sec = self._side_effect_dedupe_window_sec expired_keys = [ key for key, ts in self._recent_side_effect_actions.items() @@ -196,7 +197,7 @@ class BrainChatting: del self._recent_side_effect_actions[key] def _is_duplicate_side_effect_action(self, key: str, now: float) -> bool: - dedupe_window_sec = 120.0 + dedupe_window_sec = self._side_effect_dedupe_window_sec last_ts = self._recent_side_effect_actions.get(key) return last_ts is not None and now - last_ts <= dedupe_window_sec From ad4bfb739ff7ceeefcc85196c8967aa6862c2368 Mon Sep 17 00:00:00 2001 From: xiaoxi68 <3520824673@qq.com> Date: Sat, 7 Feb 2026 13:17:52 +0800 Subject: [PATCH 6/8] 1 --- src/chat/brain_chat/brain_chat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/brain_chat/brain_chat.py b/src/chat/brain_chat/brain_chat.py index 8bbf1d59..cc8d7ce2 100644 --- a/src/chat/brain_chat/brain_chat.py +++ b/src/chat/brain_chat/brain_chat.py @@ -104,7 +104,7 @@ class BrainChatting: # side-effect 动作幂等缓存,避免同一触发消息在短时间内重复执行。 self._recent_side_effect_actions: Dict[str, float] = {} - self._side_effect_dedupe_window_sec = 20.0 + self._side_effect_dedupe_window_sec = 100.0 async def start(self): """检查是否需要启动主循环,如果未激活则启动。""" From c92581afe76e6d84ec6c9d36c1ac3f0e27cecbc5 Mon Sep 17 00:00:00 2001 From: xiaoxi68 <3520824673@qq.com> Date: Sat, 7 Feb 2026 19:48:11 +0800 Subject: [PATCH 7/8] =?UTF-8?q?brain=5Fchat=E6=97=A0=E6=96=B0=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E9=99=90=E5=AE=9A=E5=8A=A8=E4=BD=9C:reply/wait,comple?= =?UTF-8?q?te=5Ftalk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/brain_chat/brain_chat.py | 14 ++++---- src/chat/brain_chat/brain_planner.py | 52 ++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/chat/brain_chat/brain_chat.py b/src/chat/brain_chat/brain_chat.py index cc8d7ce2..c5353eea 100644 --- a/src/chat/brain_chat/brain_chat.py +++ b/src/chat/brain_chat/brain_chat.py @@ -104,7 +104,7 @@ class BrainChatting: # side-effect 动作幂等缓存,避免同一触发消息在短时间内重复执行。 self._recent_side_effect_actions: Dict[str, float] = {} - self._side_effect_dedupe_window_sec = 100.0 + self._side_effect_dedupe_window_sec = 10.0 async def start(self): """检查是否需要启动主循环,如果未激活则启动。""" @@ -213,13 +213,11 @@ class BrainChatting: filter_intercept_message_level=1, ) - # 没有新增用户消息时,直接等待下一轮,避免对旧上下文反复规划。 - if not recent_messages_list: - await asyncio.sleep(0.2) - return True - - self.last_read_time = time.time() - self._new_message_event.set() # 触发新消息事件,打断正在进行的 wait + # 仅在有新消息时更新读取时间并触发事件。 + # 无新消息时仍允许继续思考,具体动作由 Planner 限制为 reply/wait。 + if recent_messages_list: + self.last_read_time = time.time() + self._new_message_event.set() # 触发新消息事件,打断正在进行的 wait should_continue = await self._observe(recent_messages_list=recent_messages_list) diff --git a/src/chat/brain_chat/brain_planner.py b/src/chat/brain_chat/brain_planner.py index 070a1f40..bdc594e4 100644 --- a/src/chat/brain_chat/brain_planner.py +++ b/src/chat/brain_chat/brain_planner.py @@ -330,14 +330,16 @@ class BrainPlanner: for msg in message_list_before_now ) if not has_new_user_message: - non_side_effect_actions = {"reply", "wait", "wait_time", "listening", "complete_talk", "no_reply"} - side_effect_actions = [a.action_type for a in actions if a.action_type not in non_side_effect_actions] - if side_effect_actions: + actions, dropped_actions = self._restrict_actions_without_new_user_message( + actions=actions, + available_actions=available_actions, + message_id_list=message_id_list, + ) + if dropped_actions: logger.info( - f"{self.log_prefix}检测到无新用户消息,跳过副作用动作: {' '.join(side_effect_actions)}" + f"{self.log_prefix}检测到无新用户消息,仅保留 reply/wait/complete_talk,移除动作: {' '.join(dropped_actions)}" ) - actions = self._create_complete_talk("没有新的用户消息,跳过副作用动作", available_actions) - reasoning = f"{reasoning};检测到无新用户消息,已跳过副作用动作" + reasoning = f"{reasoning};检测到无新用户消息,仅保留 reply/wait/complete_talk" # 记录和展示计划日志 logger.info( @@ -611,6 +613,44 @@ class BrainPlanner: ) ] + def _restrict_actions_without_new_user_message( + self, + actions: List[ActionPlannerInfo], + available_actions: Dict[str, ActionInfo], + message_id_list: List[Tuple[str, "DatabaseMessages"]], + ) -> Tuple[List[ActionPlannerInfo], List[str]]: + """无新用户消息时,仅保留 reply/wait/complete_talk。""" + allowed_actions: List[ActionPlannerInfo] = [] + dropped_actions: List[str] = [] + + for action in actions: + if action.action_type in {"reply", "complete_talk"}: + allowed_actions.append(action) + continue + + if action.action_type in {"wait", "listening", "wait_time"}: + action.action_type = "wait" + action.action_data = action.action_data or {} + action.action_data.setdefault("wait_seconds", 5) + allowed_actions.append(action) + continue + + dropped_actions.append(action.action_type) + + if allowed_actions: + return allowed_actions, dropped_actions + + target_message = message_id_list[-1][1] if message_id_list else None + fallback_wait = ActionPlannerInfo( + action_type="wait", + reasoning="没有新的用户消息,进入等待", + action_data={"wait_seconds": 5}, + action_message=target_message, + available_actions=available_actions, + ) + + return [fallback_wait], dropped_actions + def add_plan_log(self, reasoning: str, actions: List[ActionPlannerInfo]): """添加计划日志""" self.plan_log.append((reasoning, time.time(), actions)) From 53fb036d6ab155192aa298003033f01b5b51cdbd Mon Sep 17 00:00:00 2001 From: xiaoxi68 <3520824673@qq.com> Date: Sat, 7 Feb 2026 20:05:34 +0800 Subject: [PATCH 8/8] 1 --- src/chat/brain_chat/brain_chat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/brain_chat/brain_chat.py b/src/chat/brain_chat/brain_chat.py index c5353eea..913f6f28 100644 --- a/src/chat/brain_chat/brain_chat.py +++ b/src/chat/brain_chat/brain_chat.py @@ -625,7 +625,7 @@ class BrainChatting: self._cleanup_recent_side_effect_actions(now) if self._is_duplicate_side_effect_action(side_effect_action_key, now): logger.info( - f"{self.log_prefix} 跳过重复副作用动作: {action_planner_info.action_type}" + f"{self.log_prefix} 跳过重复动作: {action_planner_info.action_type}" ) return { "action_type": action_planner_info.action_type,