use latest group image for xiaoniu image followups

This commit is contained in:
liuwei
2026-04-07 14:00:08 +08:00
parent 7c12738967
commit acf3177571
4 changed files with 91 additions and 0 deletions

View File

@@ -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]:
"""获取指定群成员近期消息"""

View File

@@ -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])

View File

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

View File

@@ -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