diff --git a/plugins/ai_auto_response/main.py b/plugins/ai_auto_response/main.py index e1bf312..d190ed6 100644 --- a/plugins/ai_auto_response/main.py +++ b/plugins/ai_auto_response/main.py @@ -608,6 +608,21 @@ class AIAutoResponsePlugin(MessagePluginInterface): fallback_reply_mode=reply_mode, fallback_topic=trigger.topic or "", ) + # 这里补一条完整的模型决策日志: + # 1. 无论模型最终决定回还是不回,都先把 should_reply / reply_mode / topic / reply 预览记下来; + # 2. 同时保留 sanitize 后的原始响应预览,方便排查“模型其实回了什么 JSON / 文本”; + # 3. 这样后面即使被 cooldown、过期、去重拦住,也能明确看到“模型本来想怎么做”。 + self._log_event( + "llm_result", + room_id=room_id, + sender=sender, + trigger_type=trigger.trigger_type, + should_reply=bool(llm_result.get("should_reply", True)), + reply_mode=str(llm_result.get("reply_mode", reply_mode) or reply_mode), + topic=str(llm_result.get("topic_summary", "") or llm_result.get("topic_id", "") or trigger.topic or ""), + response_preview=preview_text(str(llm_result.get("reply", "") or ""), 120), + raw_response_preview=preview_text(response, 160), + ) if not llm_result.get("should_reply", True): self._log_event( "skip", @@ -639,6 +654,16 @@ class AIAutoResponsePlugin(MessagePluginInterface): # 2. 模型先统一判断 should_reply,只有当它明确想回时,才进入频率控制; # 3. 仍然保留冷却,是为了守住群内刷屏风险,但职责已经变成“限制发送”,不是“替模型做语义裁决”。 if not self.cooldown.pass_cooldown(room_id, sender, trigger.__dict__): + self._log_event( + "blocked_reply", + room_id=room_id, + sender=sender, + reason=f"post_llm_{trigger.__dict__.get('_cooldown_reason', 'cooldown')}", + trigger_type=trigger.trigger_type, + reply_mode=reply_mode, + topic=selected_topic, + response_preview=preview_text(reply_text, 120), + ) self._log_event( "skip", room_id=room_id, @@ -657,6 +682,17 @@ class AIAutoResponsePlugin(MessagePluginInterface): # 2. 即使消息进模型时还新鲜,等模型回完也可能已经跟不上群聊了; # 3. 这种情况下直接放弃发送,比突然补回旧话更自然。 if self._is_message_stale(message): + self._log_event( + "blocked_reply", + room_id=room_id, + sender=sender, + reason="stale_before_send", + trigger_type=trigger.trigger_type, + reply_mode=reply_mode, + topic=selected_topic, + response_preview=preview_text(final_response_text, 120), + age_sec=round(self._get_message_queue_age_sec(message), 2), + ) self._log_event( "skip", room_id=room_id, @@ -673,6 +709,16 @@ class AIAutoResponsePlugin(MessagePluginInterface): # 2. 这时即使模型产出了结果,也不应该再把旧回复补发出去; # 3. 直接丢弃旧结果,让群里只看到贴着最新现场的回复。 if self._is_message_superseded(message): + self._log_event( + "blocked_reply", + room_id=room_id, + sender=sender, + reason="superseded_before_send", + trigger_type=trigger.trigger_type, + reply_mode=reply_mode, + topic=selected_topic, + response_preview=preview_text(final_response_text, 120), + ) self._log_event( "skip", room_id=room_id, @@ -690,6 +736,16 @@ class AIAutoResponsePlugin(MessagePluginInterface): reply_text=final_response_text, expiry_sec=reply_dedup_expiry, ): + self._log_event( + "blocked_reply", + room_id=room_id, + sender=sender, + reason="duplicate_reply", + trigger_type=trigger.trigger_type, + reply_mode=reply_mode, + topic=selected_topic, + response_preview=preview_text(final_response_text, 120), + ) self._log_event( "skip", room_id=room_id, diff --git a/plugins/ai_auto_response/runtime/logging.py b/plugins/ai_auto_response/runtime/logging.py index c9353a3..555f348 100644 --- a/plugins/ai_auto_response/runtime/logging.py +++ b/plugins/ai_auto_response/runtime/logging.py @@ -76,6 +76,31 @@ def build_log_summary(event: str, data: Dict[str, Any]) -> str: f"err={data.get('last_error', '')}" ).strip() + if event == "llm_result": + return ( + f"[XIAONIU] LLM_RESULT room={room} user={sender} " + f"trigger={data.get('trigger_type', 'none')} " + f"want={yn(data.get('should_reply'))} " + f"mode={data.get('reply_mode', '')} " + f"topic={data.get('topic', '-') or '-'} " + f"reply={data.get('response_preview', '')} " + f"raw={data.get('raw_response_preview', '')}" + ).strip() + + if event == "blocked_reply": + age_text = "" + if data.get("age_sec") not in (None, ""): + age_text = f" age={data.get('age_sec')}" + return ( + f"[XIAONIU] BLOCKED_REPLY room={room} user={sender} " + f"reason={data.get('reason', '')} " + f"trigger={data.get('trigger_type', 'none')} " + f"mode={data.get('reply_mode', '')} " + f"topic={data.get('topic', '-') or '-'} " + f"reply={data.get('response_preview', '')}" + f"{age_text}" + ).strip() + if event == "sent": return ( f"[XIAONIU] SENT room={room} user={sender_name}/{sender} "