diff --git a/plugins/ai_auto_response/main.py b/plugins/ai_auto_response/main.py index bcb1fec..8924742 100644 --- a/plugins/ai_auto_response/main.py +++ b/plugins/ai_auto_response/main.py @@ -166,11 +166,18 @@ class AIAutoResponsePlugin(MessagePluginInterface): "sender": sender, "sender_name": sender_name, "content": content, + "is_at": bool(message.get("is_at", False)), "timestamp": message.get("timestamp"), } self._append_group_message(room_id, normalized_message) recent_messages = self.group_messages.get(room_id) or self.memory_store.get_recent_messages(room_id) - conversation_hints = self._build_conversation_hints(recent_messages, sender, content) + conversation_hints = self._build_conversation_hints( + recent_messages, + sender, + content, + quote_context, + self.persona_engine.config.get("name", "小牛"), + ) memory_hints = self.memory_store.build_memory_hints(room_id, sender) self._sync_member_memory(room_id, sender, sender_name, memory_hints.get("member_context", {})) @@ -183,7 +190,7 @@ class AIAutoResponsePlugin(MessagePluginInterface): is_followup=memory_hints.get("is_followup", False), last_active_at=memory_hints.get("last_active_at", "") or "", ) - trigger = self.trigger_router.route(message | {"content": content}, memory_hints) + trigger = self.trigger_router.route(message | {"content": content}, memory_hints, conversation_hints) flow_state = self.flow_manager.apply_message_event(room_id, { "is_at": message.get("is_at", False), "is_question": trigger.is_question, @@ -200,6 +207,7 @@ class AIAutoResponsePlugin(MessagePluginInterface): trigger_type=trigger.trigger_type, priority=trigger.priority, reasons="|".join(trigger.reasons), + directed=self._yn(trigger.is_directed), flow_state=flow_state.state, flow_score=round(flow_state.score, 2), topic=trigger.topic or "", @@ -299,6 +307,7 @@ class AIAutoResponsePlugin(MessagePluginInterface): return False, "empty_response" reply_chunks = self._finalize_reply(response, reply_mode) + reply_chunks = self._maybe_add_name_prefix(reply_chunks, sender_name, reply_mode, trigger.__dict__) for chunk in reply_chunks: await bot.send_text_message(room_id, chunk, sender) @@ -435,7 +444,13 @@ class AIAutoResponsePlugin(MessagePluginInterface): ) @staticmethod - def _build_conversation_hints(recent_messages: List[Dict], current_sender: str, current_content: str) -> Dict[str, Any]: + def _build_conversation_hints( + recent_messages: List[Dict], + current_sender: str, + current_content: str, + quote_context: Dict[str, Any], + bot_name: str, + ) -> Dict[str, Any]: previous_messages = list(recent_messages[:-1]) if recent_messages else [] recent_window = previous_messages[-4:] solver_count = 0 @@ -449,9 +464,28 @@ class AIAutoResponsePlugin(MessagePluginInterface): if AIAutoResponsePlugin._looks_like_answer(content) and AIAutoResponsePlugin._has_topic_overlap(current_tokens, content): solver_count += 1 solver_senders.add(sender) + previous_same_sender_directed = False + same_sender_recent_count = 0 + bot_name_lower = str(bot_name or "").lower() + for item in reversed(previous_messages[-6:]): + sender = str(item.get("sender", "") or "") + if sender != current_sender: + continue + same_sender_recent_count += 1 + content = str(item.get("content") or item.get("message") or "").strip().lower() + if bool(item.get("is_at")) or (bot_name_lower and bot_name_lower in content): + previous_same_sender_directed = True + break + quote_targets_bot = False + quote_sender_name = str(quote_context.get("quote_sender_name", "") or "").strip().lower() + if quote_sender_name and bot_name_lower and bot_name_lower in quote_sender_name: + quote_targets_bot = True return { "has_recent_human_solver": solver_count >= 2 and len(solver_senders) >= 1, "solver_count": solver_count, + "previous_same_sender_directed": previous_same_sender_directed, + "same_sender_recent_count": same_sender_recent_count, + "quote_targets_bot": quote_targets_bot, } @staticmethod @@ -542,6 +576,41 @@ class AIAutoResponsePlugin(MessagePluginInterface): break return chunks[:chunk_limit] or [text[:char_limit].strip()] + def _maybe_add_name_prefix(self, reply_chunks: List[str], sender_name: str, reply_mode: str, trigger: Dict[str, Any]) -> List[str]: + if not reply_chunks: + return reply_chunks + if reply_mode not in {"social_short", "qa_fast", "qa_with_context"}: + return reply_chunks + if not (trigger.get("is_at") or trigger.get("is_directed") or trigger.get("is_social_call")): + return reply_chunks + clean_name = self._clean_display_name(sender_name) + if not clean_name or len(clean_name) > 8: + return reply_chunks + first = reply_chunks[0].strip() + if not first or clean_name in first: + return reply_chunks + # 低频昵称点名:按内容稳定分桶,避免每次都叫,也避免完全随机飘忽。 + bucket_seed = f"{clean_name}|{reply_mode}|{first}" + if (sum(ord(ch) for ch in bucket_seed) % 5) != 0: + return reply_chunks + prefix = f"{clean_name}," + if len(first) + len(prefix) > (16 if reply_mode == "social_short" else 42): + return reply_chunks + updated = list(reply_chunks) + updated[0] = prefix + first + return updated + + @staticmethod + def _clean_display_name(sender_name: str) -> str: + text = str(sender_name or "").strip() + if not text: + return "" + text = re.sub(r"\s+", "", text) + text = re.sub(r"[^\u4e00-\u9fffA-Za-z0-9_]", "", text) + if not text: + return "" + return text[:8] + def _sync_member_memory(self, room_id: str, sender: str, sender_name: str, member_context: Dict) -> None: if not member_context: @@ -644,6 +713,7 @@ class AIAutoResponsePlugin(MessagePluginInterface): return ( f"[XIAONIU] DECIDE room={room} user={sender} " f"trigger={data.get('trigger_type', 'none')} " + f"dir={data.get('directed', '-') or '-'} " f"flow={data.get('flow_state', '')}:{data.get('flow_score', '')} " f"topic={data.get('topic', '-') or '-'} " f"reasons={data.get('reasons', '-') or '-'}" diff --git a/plugins/ai_auto_response/triggers.py b/plugins/ai_auto_response/triggers.py index 1f9b356..82cbdcd 100644 --- a/plugins/ai_auto_response/triggers.py +++ b/plugins/ai_auto_response/triggers.py @@ -35,7 +35,8 @@ class TriggerRouter: self.config = config or {} self.topic_keywords = [str(item).lower() for item in self.config.get("focus", [])] - def route(self, message: Dict, memory_hints: Dict) -> TriggerResult: + def route(self, message: Dict, memory_hints: Dict, conversation_hints: Dict | None = None) -> TriggerResult: + conversation_hints = conversation_hints or {} content = str(message.get("content", "")).strip() content_lower = content.lower() result = TriggerResult() @@ -80,6 +81,17 @@ class TriggerRouter: result.priority = max(result.priority, float(self.config.get("light_social", 0.45))) result.is_social_call = True result.reasons.append("light_social") + if conversation_hints.get("quote_targets_bot"): + result.is_directed = True + result.should_respond = True + result.reasons.append("quote_targets_bot") + if result.trigger_type == "none": + result.trigger_type = "quote_followup_trigger" + result.priority = max(result.priority, float(self.config.get("followup", 0.90))) + if conversation_hints.get("previous_same_sender_directed") and result.is_question: + result.is_directed = True + result.should_respond = True + result.reasons.append("same_sender_directed") if result.is_question and result.is_directed: result.should_respond = True if memory_hints.get("returning_member_state") in {"returning_member", "long_absent_member"}: