improve xiaoniu recipient cues and name-prefixed replies

This commit is contained in:
liuwei
2026-04-07 15:32:38 +08:00
parent 9e95b805ec
commit 19d2938870
2 changed files with 86 additions and 4 deletions

View File

@@ -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 '-'}"

View File

@@ -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"}: