This commit is contained in:
Windpicker-owo
2025-12-02 13:05:19 +08:00
3 changed files with 114 additions and 17 deletions

View File

@@ -494,6 +494,14 @@ class StreamLoopManager:
logger.debug(f"{stream_id} 未读消息为空,跳过 chatter 处理") logger.debug(f"{stream_id} 未读消息为空,跳过 chatter 处理")
return True # 返回 True 表示处理完成(虽然没有实际处理) 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)} 条未读消息,开始处理") logger.debug(f"{stream_id}{len(unread_messages)} 条未读消息,开始处理")
# 设置触发用户ID以实现回复保护 # 设置触发用户ID以实现回复保护
@@ -537,6 +545,70 @@ class StreamLoopManager:
# 无论成功或失败,都要设置处理状态为未处理 # 无论成功或失败,都要设置处理状态为未处理
self._set_stream_processing_status(stream_id, False) 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: def _set_stream_processing_status(self, stream_id: str, is_processing: bool) -> None:
"""设置流的处理状态""" """设置流的处理状态"""
try: try:
@@ -643,6 +715,18 @@ class StreamLoopManager:
if global_config is None: if global_config is None:
raise RuntimeError("Global config is not initialized") 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) base_interval = getattr(global_config.chat, "distribution_interval", 5.0)

View File

@@ -178,13 +178,16 @@ class KokoroFlowChatter(BaseChatter):
# 10. 执行动作 # 10. 执行动作
exec_results = [] exec_results = []
has_reply = False has_reply = False
for action in plan_response.actions: for action in plan_response.actions:
action_data = action.params.copy()
result = await self.action_manager.execute_action( result = await self.action_manager.execute_action(
action_name=action.type, action_name=action.type,
chat_id=self.stream_id, chat_id=self.stream_id,
target_message=target_message, target_message=target_message,
reasoning=plan_response.thought, reasoning=plan_response.thought,
action_data=action.params, action_data=action_data,
thinking_id=None, thinking_id=None,
log_prefix="[KFC]", log_prefix="[KFC]",
) )

View File

@@ -180,7 +180,14 @@ def build_actions_module(available_actions: Optional[dict[str, ActionInfo]] = No
if not available_actions: if not available_actions:
return _get_default_actions_block() 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(): for action_name, action_info in available_actions.items():
description = action_info.description or f"执行 {action_name}" 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}" action_block = f"### `{action_name}` - {description}"
# 对 kfc_reply 特殊处理,再次强调限制
if action_name == "kfc_reply":
action_block += "\n(只能有一个,内容写完整)"
# 参数说明(如果有) # 参数说明(如果有)
if action_info.action_parameters: if action_info.action_parameters:
params_lines = [f" - `{name}`: {desc}" for name, desc in action_info.action_parameters.items()] 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: def _get_default_actions_block() -> str:
"""获取默认的内置动作描述块""" """获取默认的内置动作描述块"""
return """### `kfc_reply` - 发消息 return """⚠️ **输出限制(必须遵守)**
发送文字回复。**注意:只能有一个 kfc_reply 动作,把你想说的话都写在一条消息里。** 1. `actions` 数组里**只能有一个** `kfc_reply`,不能写多个
2. `kfc_reply` 的 `content` 要简洁,像发微信一样,**不要写长篇大论**
3. 系统会自动把你的回复拆分成多条消息发送,你不需要自己分段
### `kfc_reply` - 发消息
```json ```json
{"type": "kfc_reply", "content": "说的话,全部写在这里"} {"type": "kfc_reply", "content": "说的话"}
``` ```
### `poke_user` - 戳一戳 ### `poke_user` - 戳一戳
戳对方一下
```json ```json
{"type": "poke_user"} {"type": "poke_user"}
``` ```
### `update_internal_state` - 更新你的心情
更新你现在的心情状态
```json
{"type": "update_internal_state", "mood": "开心"}
```
### `do_nothing` - 不做任何事 ### `do_nothing` - 不做任何事
想了想,决定现在不作回应
```json ```json
{"type": "do_nothing"} {"type": "do_nothing"}
```""" ```"""
@@ -262,13 +269,16 @@ def build_output_module(context_data: Optional[dict[str, str]] = None) -> str:
# JSON 输出格式说明(更自然的思考引导) # JSON 输出格式说明(更自然的思考引导)
json_format = """### 输出格式JSON json_format = """### 输出格式JSON
⚠️ **核心规则**actions 中只能有**一个** `kfc_reply`动作!不能有多个`kfc_reply`动作!想说的话全写在一条消息里,系统会自动拆分发送。
```json ```json
{ {
"thought": "你心里的真实想法,像日记一样自然", "thought": "你心里的真实想法,像日记一样自然",
"expected_user_reaction": "猜猜对方看到会怎么想", "expected_user_reaction": "猜猜对方看到会怎么想",
"max_wait_seconds": "预估的等待时间(秒)", "max_wait_seconds": "预估的等待时间(秒)",
"actions": [ "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内心想法 关于 thought内心想法
- 写你真正在想的,不是在分析任务 - 写你真正在想的,不是在分析任务
- 像心里嘀咕一样,比如"这家伙又来撒娇了~" "有点困了但还想再聊会儿" - 像心里嘀咕一样,比如"这家伙又来撒娇了" "有点困了但还想再聊会儿"
- 不要写"根据设定""我需要""我应该"这种规划性的话 - 不要写"根据设定""我需要""我应该"这种规划性的话
- 就是你作为这个人,此刻心里在想什么 - 就是你作为这个人,此刻心里在想什么
@@ -350,10 +360,10 @@ def build_system_prompt(
"## 3. 现在的情况", "## 3. 现在的情况",
build_context_module(session, chat_stream, context_data), build_context_module(session, chat_stream, context_data),
"", "",
"## 5. 你能做的事", "## 4. 你能做的事",
build_actions_module(available_actions), build_actions_module(available_actions),
"", "",
"## 6. 怎么回复", "## 5. 怎么回复",
build_output_module(context_data), build_output_module(context_data),
] ]