From 1027c5abf7be9533c478358ff05ee433fee3f562 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Tue, 2 Dec 2025 01:40:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E6=B7=BB=E5=8A=A0=E7=BE=A4?= =?UTF-8?q?=E7=BB=84=E9=9D=99=E9=9F=B3=E5=8A=9F=E8=83=BD=E5=B9=B6=E6=8F=90?= =?UTF-8?q?=E5=8D=87=E7=A7=81=E8=81=8A=E5=93=8D=E5=BA=94=E9=80=9F=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 此提交引入了消息处理的两个主要增强功能: 1. **群组静音功能**: 新增的 `mute_group_list` 配置允许指定机器人默认保持沉默的群组。仅在被明确提及(通过@、回复或使用其名称/别名)时,它才会在这些群组中响应,从而减少繁忙频道的噪音。 2. **私聊响应能力**: 消息分发循环现在会动态调整轮询间隔。对于私聊,它使用更短的间隔,从而实现显著更快、接近实时的响应。 此外,此次提交还包括 Kokoro Flow 聊天器的若干改进: - refactor(kokoro-flow):系统提示进行了大幅修订,以强制执行单个 `kfc_reply` 动作,引导 LLM 生成更可靠且格式正确的输出。 - fix(kokoro-flow):在执行动作时使用 `action.params.copy()`,以防止潜在的副作用来自下游的修改。 --- .../message_manager/distribution_manager.py | 84 +++++++++++++++++++ .../built_in/kokoro_flow_chatter/chatter.py | 5 +- .../prompt_modules_unified.py | 42 ++++++---- 3 files changed, 114 insertions(+), 17 deletions(-) diff --git a/src/chat/message_manager/distribution_manager.py b/src/chat/message_manager/distribution_manager.py index fc43e0959..a7bd24f69 100644 --- a/src/chat/message_manager/distribution_manager.py +++ b/src/chat/message_manager/distribution_manager.py @@ -494,6 +494,14 @@ class StreamLoopManager: logger.debug(f"流 {stream_id} 未读消息为空,跳过 chatter 处理") return True # 返回 True 表示处理完成(虽然没有实际处理) + # 🔇 静默群组检查:在静默群组中,只有提到 Bot 名字/别名才响应 + if await self._should_skip_for_mute_group(stream_id, unread_messages): + # 清空未读消息,不触发 chatter + from .message_manager import message_manager + await message_manager.clear_stream_unread_messages(stream_id) + logger.debug(f"🔇 流 {stream_id} 在静默列表中且未提及Bot,跳过处理") + return True + logger.debug(f"流 {stream_id} 有 {len(unread_messages)} 条未读消息,开始处理") # 设置触发用户ID,以实现回复保护 @@ -537,6 +545,70 @@ class StreamLoopManager: # 无论成功或失败,都要设置处理状态为未处理 self._set_stream_processing_status(stream_id, False) + async def _should_skip_for_mute_group(self, stream_id: str, unread_messages: list) -> bool: + """检查是否应该因静默群组而跳过处理 + + 在静默群组中,只有当消息提及 Bot(@、回复、包含名字/别名)时才响应。 + + Args: + stream_id: 流ID + unread_messages: 未读消息列表 + + Returns: + bool: True 表示应该跳过,False 表示正常处理 + """ + if global_config is None: + return False + + # 获取静默群组列表 + mute_group_list = getattr(global_config.message_receive, "mute_group_list", []) + if not mute_group_list: + return False + + try: + # 获取 chat_stream 来检查群组信息 + chat_manager = get_chat_manager() + chat_stream = await chat_manager.get_stream(stream_id) + + if not chat_stream or not chat_stream.group_info: + # 不是群聊,不适用静默规则 + return False + + group_id = str(chat_stream.group_info.group_id) + if group_id not in mute_group_list: + # 不在静默列表中 + return False + + # 在静默列表中,检查是否有消息提及 Bot + bot_name = getattr(global_config.bot, "nickname", "") + bot_aliases = getattr(global_config.bot, "alias_names", []) + bot_qq = str(getattr(global_config.bot, "qq_account", "")) + + # 构建需要检测的关键词列表 + mention_keywords = [bot_name] + list(bot_aliases) if bot_name else list(bot_aliases) + mention_keywords = [k for k in mention_keywords if k] # 过滤空字符串 + + for msg in unread_messages: + # 检查是否被 @ 或回复 + if getattr(msg, "is_at", False) or getattr(msg, "is_mentioned", False): + logger.debug(f"🔇 静默群组 {group_id}: 消息被@或回复,允许响应") + return False + + # 检查消息内容是否包含 Bot 名字或别名 + content = getattr(msg, "processed_plain_text", "") or getattr(msg, "display_message", "") or "" + for keyword in mention_keywords: + if keyword and keyword in content: + logger.debug(f"🔇 静默群组 {group_id}: 消息包含关键词 '{keyword}',允许响应") + return False + + # 没有任何消息提及 Bot + logger.debug(f"🔇 静默群组 {group_id}: {len(unread_messages)} 条消息均未提及Bot,跳过") + return True + + except Exception as e: + logger.warning(f"检查静默群组时出错: {stream_id}, error={e}") + return False + def _set_stream_processing_status(self, stream_id: str, is_processing: bool) -> None: """设置流的处理状态""" try: @@ -643,6 +715,18 @@ class StreamLoopManager: if global_config is None: raise RuntimeError("Global config is not initialized") + # 私聊使用最小间隔,快速响应 + try: + chat_manager = get_chat_manager() + chat_stream = await chat_manager.get_stream(stream_id) + if chat_stream and not chat_stream.group_info: + # 私聊:有消息时几乎立即响应,空转时稍微等待 + min_interval = 0.1 if has_messages else 3.0 + logger.debug(f"流 {stream_id} 私聊模式,使用最小间隔: {min_interval:.2f}s") + return min_interval + except Exception as e: + logger.debug(f"检查流 {stream_id} 是否为私聊失败: {e}") + # 基础间隔 base_interval = getattr(global_config.chat, "distribution_interval", 5.0) diff --git a/src/plugins/built_in/kokoro_flow_chatter/chatter.py b/src/plugins/built_in/kokoro_flow_chatter/chatter.py index 2ec77aed2..832b0cd7c 100644 --- a/src/plugins/built_in/kokoro_flow_chatter/chatter.py +++ b/src/plugins/built_in/kokoro_flow_chatter/chatter.py @@ -178,13 +178,16 @@ class KokoroFlowChatter(BaseChatter): # 10. 执行动作 exec_results = [] has_reply = False + for action in plan_response.actions: + action_data = action.params.copy() + result = await self.action_manager.execute_action( action_name=action.type, chat_id=self.stream_id, target_message=target_message, reasoning=plan_response.thought, - action_data=action.params, + action_data=action_data, thinking_id=None, log_prefix="[KFC]", ) diff --git a/src/plugins/built_in/kokoro_flow_chatter/prompt_modules_unified.py b/src/plugins/built_in/kokoro_flow_chatter/prompt_modules_unified.py index 290298d22..e7b095a9e 100644 --- a/src/plugins/built_in/kokoro_flow_chatter/prompt_modules_unified.py +++ b/src/plugins/built_in/kokoro_flow_chatter/prompt_modules_unified.py @@ -180,7 +180,14 @@ def build_actions_module(available_actions: Optional[dict[str, ActionInfo]] = No if not available_actions: return _get_default_actions_block() - action_blocks = [] + # 核心限制说明(放在最前面) + action_blocks = [ + """⚠️ **输出限制(必须遵守)**: +1. `actions` 数组里**只能有一个** `kfc_reply`,不能写多个 +2. `kfc_reply` 的 `content` 要简洁,像发微信一样,**不要写长篇大论** +3. 系统会自动把你的回复拆分成多条消息发送,你不需要自己分段 +""" + ] for action_name, action_info in available_actions.items(): description = action_info.description or f"执行 {action_name}" @@ -188,6 +195,10 @@ def build_actions_module(available_actions: Optional[dict[str, ActionInfo]] = No # 构建动作块 action_block = f"### `{action_name}` - {description}" + # 对 kfc_reply 特殊处理,再次强调限制 + if action_name == "kfc_reply": + action_block += "\n(只能有一个,内容写完整)" + # 参数说明(如果有) if action_info.action_parameters: params_lines = [f" - `{name}`: {desc}" for name, desc in action_info.action_parameters.items()] @@ -213,26 +224,22 @@ def build_actions_module(available_actions: Optional[dict[str, ActionInfo]] = No def _get_default_actions_block() -> str: """获取默认的内置动作描述块""" - return """### `kfc_reply` - 发消息 -发送文字回复。**注意:只能有一个 kfc_reply 动作,把你想说的话都写在一条消息里。** + return """⚠️ **输出限制(必须遵守)**: +1. `actions` 数组里**只能有一个** `kfc_reply`,不能写多个 +2. `kfc_reply` 的 `content` 要简洁,像发微信一样,**不要写长篇大论** +3. 系统会自动把你的回复拆分成多条消息发送,你不需要自己分段 + +### `kfc_reply` - 发消息 ```json -{"type": "kfc_reply", "content": "你要说的话,全部写在这里"} +{"type": "kfc_reply", "content": "你想说的话"} ``` ### `poke_user` - 戳一戳 -戳对方一下 ```json {"type": "poke_user"} ``` -### `update_internal_state` - 更新你的心情 -更新你现在的心情状态 -```json -{"type": "update_internal_state", "mood": "开心"} -``` - ### `do_nothing` - 不做任何事 -想了想,决定现在不作回应 ```json {"type": "do_nothing"} ```""" @@ -262,13 +269,16 @@ def build_output_module(context_data: Optional[dict[str, str]] = None) -> str: # JSON 输出格式说明(更自然的思考引导) json_format = """### 输出格式(JSON) + +⚠️ **核心规则**:actions 中只能有**一个** `kfc_reply`动作!不能有多个`kfc_reply`动作!想说的话全写在一条消息里,系统会自动拆分发送。 + ```json { "thought": "你心里的真实想法,像日记一样自然", "expected_user_reaction": "猜猜对方看到会怎么想", "max_wait_seconds": "预估的等待时间(秒)", "actions": [ - {"type": "kfc_reply", "content": "你要说的话"} + {"type": "kfc_reply", "content": "你想说的所有话,写在这里面"} ] } ``` @@ -280,7 +290,7 @@ def build_output_module(context_data: Optional[dict[str, str]] = None) -> str: 关于 thought(内心想法): - 写你真正在想的,不是在分析任务 -- 像心里嘀咕一样,比如"这家伙又来撒娇了~" "有点困了但还想再聊会儿" +- 像心里嘀咕一样,比如"这家伙又来撒娇了" "有点困了但还想再聊会儿" - 不要写"根据设定""我需要""我应该"这种规划性的话 - 就是你作为这个人,此刻心里在想什么 @@ -350,10 +360,10 @@ def build_system_prompt( "## 3. 现在的情况", build_context_module(session, chat_stream, context_data), "", - "## 5. 你能做的事", + "## 4. 你能做的事", build_actions_module(available_actions), "", - "## 6. 怎么回复", + "## 5. 怎么回复", build_output_module(context_data), ]