diff --git a/db/message_storage.py b/db/message_storage.py index 56d20ab..326b130 100644 --- a/db/message_storage.py +++ b/db/message_storage.py @@ -43,6 +43,24 @@ class MessageStorageDB(BaseDBOperator): params = (hours_ago, group_id, min_content_length) return self.execute_query(sql, params) or [] + def get_latest_image_message(self, group_id: str, before_timestamp: str = "", hours_ago: int = 8) -> Optional[Dict]: + """获取指定群最近一条已落盘图片消息""" + sql = """ + SELECT timestamp, sender, content, message_type, image_path + FROM messages + WHERE timestamp >= DATE_SUB(NOW(), INTERVAL %s HOUR) + AND group_id = %s + AND message_type = 3 + AND image_path IS NOT NULL + AND image_path <> '' + """ + params: List = [hours_ago, group_id] + if before_timestamp: + sql += " AND timestamp <= %s" + params.append(before_timestamp) + sql += " ORDER BY timestamp DESC LIMIT 1" + return self.execute_query(sql, tuple(params), fetch_one=True) + def get_member_recent_messages(self, group_id: str, wxid: str, days: int = 30, limit: int = 200, include_today: bool = True) -> List[Dict]: """获取指定群成员近期消息""" diff --git a/plugins/ai_auto_response/context_builder.py b/plugins/ai_auto_response/context_builder.py index 5ad747d..d1f5093 100644 --- a/plugins/ai_auto_response/context_builder.py +++ b/plugins/ai_auto_response/context_builder.py @@ -22,6 +22,7 @@ class ContextBuilder: reply_mode: str, vector_memories: List[Dict], quote_context: Dict | None = None, + image_context: Dict | None = None, ) -> Dict: recent_lines = [] for item in recent_messages[-self.recent_context_size:]: @@ -45,6 +46,7 @@ class ContextBuilder: "vector_memory_prompt": self._build_vector_memory_prompt(vector_memories), "group_profile_prompt": self._build_group_profile_prompt(group_profile or {}), "quote_prompt": self._build_quote_prompt(quote_context or {}), + "image_prompt": self._build_image_prompt(image_context or {}), "current_message": f"{sender_name}: {content}", } @@ -136,3 +138,14 @@ class ContextBuilder: f"被引用内容:{quote_body}" if quote_body else "", ] return "\n".join([line for line in lines if line]) + + @staticmethod + def _build_image_prompt(image_context: Dict) -> str: + if not image_context: + return "" + lines = [ + "已附带最近一张群图片作为上下文。", + f"图片发送者:{image_context.get('sender_name', '未知成员')}", + f"图片说明:{image_context.get('hint', '')}" if image_context.get("hint") else "", + ] + return "\n".join([line for line in lines if line]) diff --git a/plugins/ai_auto_response/main.py b/plugins/ai_auto_response/main.py index ba4c5e6..0e095d4 100644 --- a/plugins/ai_auto_response/main.py +++ b/plugins/ai_auto_response/main.py @@ -241,7 +241,12 @@ class AIAutoResponsePlugin(MessagePluginInterface): vector_memories = [] if self.vector_memory.should_search(reply_mode, trigger.trigger_type, memory_hints.get("returning_member_state", "")): vector_memories = self.vector_memory.search(content, room_id, sender) + image_context = self._build_recent_image_context(message, room_id, content, quote_context) image_urls = await self._prepare_quote_image_inputs(bot, quote_context) + if not image_urls and image_context: + recent_image_url = self._build_local_image_data_url(str(image_context.get("image_path", "") or "")) + if recent_image_url: + image_urls = [recent_image_url] self._log_event( "context", room_id=room_id, @@ -268,6 +273,7 @@ class AIAutoResponsePlugin(MessagePluginInterface): reply_mode=reply_mode, vector_memories=vector_memories, quote_context=quote_context | {"has_image_attachment": bool(image_urls)}, + image_context=image_context, ) system_prompt = self.persona_engine.build_system_prompt(group_profile) @@ -377,6 +383,7 @@ class AIAutoResponsePlugin(MessagePluginInterface): f"当前群聊消息:\n{recent_text}\n\n" f"当前发言:{context.get('current_message', '')}\n" f"引用补充:\n{context.get('quote_prompt', '') or '无'}\n" + f"图片补充:\n{context.get('image_prompt', '') or '无'}\n" f"触发类型:{context.get('trigger_type', 'none')}\n" f"回复模式:{context.get('reply_mode', 'social_short')}\n" f"当前心流状态:{context.get('flow_state', 'idle')}\n" @@ -725,6 +732,40 @@ class AIAutoResponsePlugin(MessagePluginInterface): return title[:220].strip() return ref_content[:220].strip() + def _build_recent_image_context( + self, + message: Dict[str, Any], + room_id: str, + content: str, + quote_context: Dict[str, str], + ) -> Dict[str, str]: + if quote_context: + return {} + if not self._is_recent_image_followup(content): + return {} + latest_image = self.memory_store.get_latest_image_message( + room_id, + before_timestamp=str(message.get("timestamp") or ""), + ) + if not latest_image: + return {} + sender = str(latest_image.get("sender", "") or "") + sender_name = self._get_sender_name(room_id, sender) if sender else "未知成员" + return { + "sender_name": sender_name, + "image_path": str(latest_image.get("image_path", "") or ""), + "hint": "用户当前这句大概率是在追问这张最近图片", + } + + @staticmethod + def _is_recent_image_followup(content: str) -> bool: + text = str(content or "").strip().lower() + if not text: + return False + image_words = ["图", "图片", "照片", "截图"] + ask_words = ["看看", "看下", "帮我看", "帮看看", "这个", "咋样", "什么", "识别", "分析"] + return any(word in text for word in image_words) and any(word in text for word in ask_words) + async def _prepare_quote_image_inputs(self, bot: WechatAPIClient, quote_context: Dict[str, str]) -> List[str]: if not quote_context or quote_context.get("quote_type_label") != "引用图片": return [] @@ -746,6 +787,21 @@ class AIAutoResponsePlugin(MessagePluginInterface): return [] return [data_url] + def _build_local_image_data_url(self, image_path: str) -> str: + if not image_path: + return "" + relative_path = image_path.lstrip("/\\").replace("/", "\\") + full_path = self.get_main_path() / relative_path + if not full_path.exists(): + return "" + try: + image_bytes = full_path.read_bytes() + except Exception: + return "" + image_type = imghdr.what(None, h=image_bytes) or "jpeg" + raw_base64 = base64.b64encode(image_bytes).decode("utf-8") + return f"data:image/{image_type};base64,{raw_base64}" + @staticmethod def _extract_quote_image_info(ref_content: str) -> Dict[str, str]: if not ref_content: diff --git a/plugins/ai_auto_response/memory_store.py b/plugins/ai_auto_response/memory_store.py index 4c0b248..16fa94b 100644 --- a/plugins/ai_auto_response/memory_store.py +++ b/plugins/ai_auto_response/memory_store.py @@ -20,6 +20,10 @@ class MemoryStore: size = int(self.config.get("recent_context_size", 30)) return recent[-size:] + def get_latest_image_message(self, room_id: str, before_timestamp: str = "") -> Optional[Dict]: + hours = int(self.config.get("active_context_hours", 8)) + return self.message_db.get_latest_image_message(room_id, before_timestamp=before_timestamp, hours_ago=hours) + def get_member_context(self, room_id: str, wxid: str) -> Optional[Dict]: if not self.config.get("enable_member_context", True): return None