From d0480691c35f8c2f9f8d60035a1381346f7a3c83 Mon Sep 17 00:00:00 2001 From: liuwei Date: Fri, 24 Apr 2026 15:37:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=9C=80=E8=BF=91=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87=E5=8F=AA=E5=8F=96=E5=88=B0=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/message_storage.py | 1 + plugins/ai_auto_response/main.py | 72 +++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/db/message_storage.py b/db/message_storage.py index 0e071e3..79dcaf2 100644 --- a/db/message_storage.py +++ b/db/message_storage.py @@ -100,6 +100,7 @@ class MessageStorageDB(BaseDBOperator): AND length(content) > %s AND CHAR_LENGTH(content) < 300 AND content NOT LIKE '/%' + ORDER BY timestamp ASC """ params = (hours_ago, group_id, min_content_length) return self.execute_query(sql, params) or [] diff --git a/plugins/ai_auto_response/main.py b/plugins/ai_auto_response/main.py index 659257c..4d7dc13 100644 --- a/plugins/ai_auto_response/main.py +++ b/plugins/ai_auto_response/main.py @@ -1,5 +1,6 @@ from __future__ import annotations import asyncio +from datetime import datetime import re import time import xml.etree.ElementTree as ET @@ -345,7 +346,7 @@ class AIAutoResponsePlugin(MessagePluginInterface): "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) + recent_messages = self._get_recent_messages_for_context(room_id) group_name_map = self._build_group_name_map(room_id) group_memory_bundle = self.group_memory.build( room_id=room_id, @@ -751,6 +752,75 @@ class AIAutoResponsePlugin(MessagePluginInterface): if len(items) > size: self.group_messages[room_id] = items[-size:] + def _get_recent_messages_for_context(self, room_id: str) -> List[Dict]: + # 最近上下文不能再走“内存 or 数据库”二选一: + # 1. 之前在 append 当前消息之后立刻读取内存,导致内存里只要有 1 条,数据库历史就完全失效; + # 2. 插件刚启动、刚切群、或该群近期还没在进程里积累消息时,就会只剩当前这一句; + # 3. 这里改成“数据库历史 + 进程内最近消息”合并,再统一去重排序,才能稳定拿到完整上下文。 + db_recent = self.memory_store.get_recent_messages(room_id) + live_recent = list(self.group_messages.get(room_id) or []) + merged = self._merge_recent_messages(db_recent, live_recent) + size = int(self.mode_config.get("recent_context_size", 30) or 30) + return merged[-max(size, 1):] + + @classmethod + def _merge_recent_messages(cls, db_recent: List[Dict], live_recent: List[Dict]) -> List[Dict]: + # 合并时优先保留更“新鲜、更完整”的内存消息: + # 1. DB 消息稳定但字段少,通常只有 sender/content/timestamp; + # 2. 内存消息会带 sender_name、is_at 等运行时字段,适合直接给模型; + # 3. 如果两边是同一条消息,就让后加入的内存版本覆盖掉 DB 的简化版本。 + merged_map: Dict[str, Dict] = {} + for item in list(db_recent or []) + list(live_recent or []): + normalized = dict(item or {}) + key = cls._build_recent_message_identity(normalized) + existing = merged_map.get(key, {}) + payload = dict(existing) + for field, value in normalized.items(): + if value not in (None, "", []): + payload[field] = value + merged_map[key] = payload + + ordered = list(merged_map.values()) + ordered.sort(key=cls._recent_message_sort_key) + return ordered + + @staticmethod + def _build_recent_message_identity(message: Dict) -> str: + sender = str(message.get("sender", "") or "").strip() + content = str(message.get("content") or message.get("message") or "").strip() + timestamp = str(message.get("timestamp", "") or "").strip() + # 这里用“时间 + 发送者 + 内容”做弱去重键: + # 1. 对同一条消息,DB 和内存版本通常会共享这三类信息; + # 2. 这样足以把“当前消息的 DB 版本”和“当前消息的内存版本”合并成一条; + # 3. 即使偶发碰撞,也只会影响完全相同内容的近似重复消息,风险可接受。 + return f"{timestamp}|{sender}|{content}" + + @classmethod + def _recent_message_sort_key(cls, message: Dict) -> tuple: + timestamp = str(message.get("timestamp", "") or "").strip() + parsed = cls._parse_recent_message_timestamp(timestamp) + sender = str(message.get("sender", "") or "").strip() + content = str(message.get("content") or message.get("message") or "").strip() + if parsed is not None: + return (0, parsed.timestamp(), sender, content) + # 没有可解析时间时,仍然给一个稳定排序键,避免不同来源顺序抖动。 + return (1, timestamp, sender, content) + + @staticmethod + def _parse_recent_message_timestamp(value: str) -> datetime | None: + text = str(value or "").strip() + if not text: + return None + for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d", "%Y/%m/%d %H:%M:%S"): + try: + return datetime.strptime(text, fmt) + except ValueError: + continue + try: + return datetime.fromtimestamp(float(text)) + except (TypeError, ValueError, OSError): + return None + def _call_llm( self, *,