From 5a02121ace62d35014d51b46847b7790fa161a40 Mon Sep 17 00:00:00 2001 From: liuwei Date: Tue, 7 Apr 2026 14:39:41 +0800 Subject: [PATCH] allow multi-part replies for xiaoniu qa responses --- plugins/ai_auto_response/main.py | 50 ++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/plugins/ai_auto_response/main.py b/plugins/ai_auto_response/main.py index c59a514..bcb1fec 100644 --- a/plugins/ai_auto_response/main.py +++ b/plugins/ai_auto_response/main.py @@ -298,13 +298,15 @@ class AIAutoResponsePlugin(MessagePluginInterface): ) return False, "empty_response" - response = self._finalize_reply(response, reply_mode) + reply_chunks = self._finalize_reply(response, reply_mode) - await bot.send_text_message(room_id, response, sender) + for chunk in reply_chunks: + await bot.send_text_message(room_id, chunk, sender) self.last_reply_at[room_id] = time.time() self.flow_manager.note_bot_reply(room_id) self.memory_store.note_bot_reply(room_id, sender, trigger.topic) - self._upsert_interaction_memory(room_id, sender, sender_name, content, response, trigger.trigger_type, trigger.topic) + final_response_text = "\n".join(reply_chunks) + self._upsert_interaction_memory(room_id, sender, sender_name, content, final_response_text, trigger.trigger_type, trigger.topic) self._log_event( "sent", room_id=room_id, @@ -312,8 +314,9 @@ class AIAutoResponsePlugin(MessagePluginInterface): sender_name=sender_name, trigger_type=trigger.trigger_type, reply_mode=reply_mode, - response_preview=self._preview(response), - response_len=len(response), + response_preview=self._preview(final_response_text), + response_len=len(final_response_text), + chunk_count=len(reply_chunks), ) return False, "replied" @@ -487,31 +490,29 @@ class AIAutoResponsePlugin(MessagePluginInterface): response = re.sub(r"\n{3,}", "\n\n", response) return response[:500].strip() - def _finalize_reply(self, response: str, reply_mode: str) -> str: + def _finalize_reply(self, response: str, reply_mode: str) -> List[str]: text = (response or "").strip() if not text: - return "" + return [] text = re.sub(r"\s+", " ", text) text = text.replace("\n", " ").strip() if reply_mode == "social_short": - text = self._take_first_sentence(text, 12) + return [self._take_first_sentence(text, 12).strip()] elif reply_mode == "qa_fast": - text = self._take_first_sentence(text, 28) + return self._split_reply_chunks(text, sentence_limit=2, char_limit=28, chunk_limit=2) elif reply_mode == "qa_with_context": - text = self._take_first_sentence(text, 36) - else: - text = self._take_first_sentence(text, 24) - return text.strip() + return self._split_reply_chunks(text, sentence_limit=2, char_limit=36, chunk_limit=2) + return [self._take_first_sentence(text, 24).strip()] @staticmethod def _build_length_rule(reply_mode: str) -> str: if reply_mode == "social_short": return "默认只回一句短话,最好控制在2到8个字,除非非常不自然。" if reply_mode == "qa_fast": - return "尽量只回1句话,总长度优先控制在28字内,先给结论,不要主动补解释。" + return "优先1句话;如果确实需要,可以拆成2条很短的话发出,总长度每条优先控制在28字内,先给结论,不要主动补解释。" if reply_mode == "qa_with_context": - return "优先控制在1句话,必要时最多2句,总长度优先控制在36字内,只给第一层答案。" + return "优先控制在1句话;必要时可以拆成2条短消息发出,每条优先控制在36字内,只给第一层答案。" return "尽量短,像群友临时接一句,不要长篇大论。" @staticmethod @@ -523,6 +524,24 @@ class AIAutoResponsePlugin(MessagePluginInterface): clipped = first[:limit].rstrip(",,、;;::") return clipped + @staticmethod + def _split_reply_chunks(text: str, sentence_limit: int, char_limit: int, chunk_limit: int) -> List[str]: + parts = [item.strip() for item in re.split(r"(?<=[。!?!?;;])", text) if item.strip()] + if not parts: + short = text.strip() + return [short[:char_limit].rstrip(",,、;;::").strip()] if short else [] + + chunks: List[str] = [] + for part in parts[:sentence_limit]: + current = part + if len(current) > char_limit: + current = current[:char_limit].rstrip(",,、;;::") + if current: + chunks.append(current.strip()) + if len(chunks) >= chunk_limit: + break + return chunks[:chunk_limit] or [text[:char_limit].strip()] + def _sync_member_memory(self, room_id: str, sender: str, sender_name: str, member_context: Dict) -> None: if not member_context: @@ -663,6 +682,7 @@ class AIAutoResponsePlugin(MessagePluginInterface): f"[XIAONIU] SENT room={room} user={sender_name}/{sender} " f"trigger={data.get('trigger_type', 'none')} " f"mode={data.get('reply_mode', '')} " + f"chunks={data.get('chunk_count', 1)} " f"len={data.get('response_len', 0)} " f"reply={data.get('response_preview', '')}" ).strip()