fix:表情包识别失败问题

This commit is contained in:
SengokuCola
2026-04-07 21:26:42 +08:00
parent f058bc3189
commit 4849c29b68
19 changed files with 1059 additions and 69 deletions

View File

@@ -3,7 +3,7 @@ MaiBot模块系统
包含聊天、情绪、记忆、日程等功能模块
"""
from src.chat.emoji_system.emoji_manager import emoji_manager
from src.emoji_system.emoji_manager import emoji_manager
from src.chat.message_receive.chat_manager import chat_manager
# 导出主要组件供外部使用

View File

@@ -296,6 +296,8 @@ class ImageManager:
async def build_image_description(self, image_bytes: bytes) -> MaiImage:
"""在图片已保存的前提下生成或补齐图片描述。"""
mai_image = await self.ensure_image_saved(image_bytes)
if not mai_image.image_format:
await mai_image.calculate_hash_format()
if mai_image.vlm_processed and mai_image.description:
return mai_image

View File

@@ -1,4 +1,4 @@
from src.chat.emoji_system.emoji_manager import emoji_manager
from src.emoji_system.emoji_manager import emoji_manager
from src.chat.message_receive.chat_manager import chat_manager

View File

@@ -265,7 +265,7 @@ class SessionMessage(MaiMessage):
"""
if component.content: # 先检查是否处理过
return component.content
from src.chat.emoji_system.emoji_manager import emoji_manager
from src.emoji_system.emoji_manager import emoji_manager
# 获取表情包描述
try:

View File

@@ -1,10 +1,11 @@
from datetime import datetime
from pathlib import Path
from typing import List, Optional
import asyncio
import hashlib
import io
import traceback
from datetime import datetime
from pathlib import Path
from typing import List, Optional
from PIL import Image as PILImage
from rich.traceback import install
@@ -28,15 +29,27 @@ class BaseImageDataModel(BaseDatabaseDataModel[Images]):
if Path(full_path).is_dir() or not Path(full_path).exists():
raise FileNotFoundError(f"表情包路径无效: {full_path}")
resolved_path = Path(full_path).absolute().resolve()
self.full_path: Path = resolved_path
self.dir_path: Path = resolved_path.parent.resolve()
self.file_name: str = resolved_path.name
self.full_path: Path
self.dir_path: Path
self.file_name: str
self._set_full_path(resolved_path)
self.file_hash: str = None # type: ignore
self.image_bytes: Optional[bytes] = image_bytes
self.image_format: str = "" # 图片格式
def _set_full_path(self, full_path: Path) -> None:
"""同步更新文件路径相关的运行时元数据。"""
resolved_path = full_path.absolute().resolve()
self.full_path = resolved_path
self.dir_path = resolved_path.parent.resolve()
self.file_name = resolved_path.name
def _restore_image_format_from_path(self) -> None:
"""根据文件扩展名恢复基础图片格式信息。"""
self.image_format = self.full_path.suffix.removeprefix(".").lower()
def read_image_bytes(self, path: Path) -> bytes:
"""
同步读取图片文件的字节内容
@@ -97,6 +110,7 @@ class BaseImageDataModel(BaseDatabaseDataModel[Images]):
image_bytes = await asyncio.to_thread(self.read_image_bytes, self.full_path)
else:
image_bytes = self.image_bytes
self.image_bytes = image_bytes
self.file_hash = hashlib.sha256(image_bytes).hexdigest()
logger.debug(f"[初始化] {self.file_name} 计算哈希值成功: {self.file_hash}")
@@ -115,7 +129,7 @@ class BaseImageDataModel(BaseDatabaseDataModel[Images]):
new_file_name = ".".join(self.file_name.split(".")[:-1] + [self.image_format])
new_full_path = self.dir_path / new_file_name
self.full_path.rename(new_full_path)
self.full_path = new_full_path
self._set_full_path(new_full_path)
return True
except Exception as e:
@@ -153,6 +167,7 @@ class MaiEmoji(BaseImageDataModel):
raise ValueError(f"数据库记录 {db_record.image_hash} 标记为文件不存在,无法创建 MaiEmoji 对象")
obj = cls(db_record.full_path)
obj.file_hash = db_record.image_hash
obj._restore_image_format_from_path()
description = db_record.description or ""
obj.description = description
normalized_tags = [
@@ -207,7 +222,8 @@ class MaiImage(BaseImageDataModel):
raise ValueError(f"数据库记录 {db_record.image_hash} 标记为文件不存在,无法创建 MaiImage 对象")
obj = cls(db_record.full_path)
obj.file_hash = db_record.image_hash
obj.full_path = Path(db_record.full_path)
obj._set_full_path(Path(db_record.full_path))
obj._restore_image_format_from_path()
obj.description = db_record.description
obj.vlm_processed = db_record.vlm_processed
return obj

View File

@@ -826,7 +826,7 @@ class EmojiManager:
Returns:
return (Tuple[bool, MaiEmoji]): 返回是否成功构建描述及表情包对象
"""
if not target_emoji.file_hash:
if not target_emoji.file_hash or not target_emoji.image_format:
# Should not happen, but just in case
await target_emoji.calculate_hash_format()

View File

@@ -7,7 +7,7 @@ import time
from src.A_memorix.host_service import a_memorix_host_service
from src.learners.expression_auto_check_task import ExpressionAutoCheckTask
from src.chat.emoji_system.emoji_manager import emoji_manager
from src.emoji_system.emoji_manager import emoji_manager
from src.chat.message_receive.bot import chat_bot
from src.chat.message_receive.chat_manager import chat_manager
from src.chat.utils.statistic import OnlineTimeRecordTask, StatisticOutputTask

View File

@@ -12,8 +12,8 @@ from PIL import Image as PILImage
from PIL import ImageDraw, ImageFont
from pydantic import BaseModel, Field as PydanticField
from src.chat.emoji_system.emoji_manager import emoji_manager
from src.chat.emoji_system.maisaka_tool import send_emoji_for_maisaka
from src.emoji_system.emoji_manager import emoji_manager
from src.emoji_system.maisaka_tool import send_emoji_for_maisaka
from src.common.data_models.image_data_model import MaiEmoji
from src.common.data_models.message_component_data_model import ImageComponent, MessageSequence, TextComponent
from src.common.logger import get_logger

View File

@@ -352,6 +352,7 @@ class MaisakaReasoningEngine:
timing_response: Optional[ChatResponse] = None
timing_tool_results: Optional[list[str]] = None
response: Optional[ChatResponse] = None
tool_result_summaries: list[str] = []
tool_monitor_results: list[dict[str, Any]] = []
try:
visual_refresh_started_at = time.time()
@@ -377,14 +378,6 @@ class MaisakaReasoningEngine:
selected_history_count=timing_response.selected_history_count,
duration_ms=timing_duration_ms,
)
self._runtime._render_context_usage_panel(
selected_history_count=timing_response.selected_history_count,
prompt_tokens=timing_response.prompt_tokens,
planner_response=timing_response.content or "",
tool_calls=timing_response.tool_calls,
tool_results=timing_tool_results,
prompt_section=timing_response.prompt_section,
)
if timing_action != "continue":
logger.info(
f"{self._runtime.log_prefix} Timing Gate 结束当前回合: "
@@ -418,7 +411,6 @@ class MaisakaReasoningEngine:
self._last_reasoning_content = reasoning_content
self._runtime._chat_history.append(response.raw_message)
tool_result_summaries: list[str] = []
tool_monitor_results = []
if response.tool_calls:
@@ -429,25 +421,10 @@ class MaisakaReasoningEngine:
anchor_message,
)
cycle_detail.time_records["tool_calls"] = time.time() - tool_started_at
self._runtime._render_context_usage_panel(
selected_history_count=response.selected_history_count,
prompt_tokens=response.prompt_tokens,
planner_response=response.content or "",
tool_calls=response.tool_calls,
tool_results=tool_result_summaries,
tool_detail_results=tool_monitor_results,
prompt_section=response.prompt_section,
)
if should_pause:
break
continue
self._runtime._render_context_usage_panel(
selected_history_count=response.selected_history_count,
prompt_tokens=response.prompt_tokens,
planner_response=response.content or "",
prompt_section=response.prompt_section,
)
if not response.content:
break
except ReqAbortException:
@@ -462,6 +439,31 @@ class MaisakaReasoningEngine:
break
finally:
completed_cycle = self._end_cycle(cycle_detail)
self._runtime._render_context_usage_panel(
cycle_id=cycle_detail.cycle_id,
timing_selected_history_count=(
timing_response.selected_history_count if timing_response is not None else None
),
timing_prompt_tokens=(
timing_response.prompt_tokens if timing_response is not None else None
),
timing_action=timing_action or "",
timing_response=timing_response.content or "" if timing_response is not None else "",
timing_tool_calls=timing_response.tool_calls if timing_response is not None else None,
timing_tool_results=timing_tool_results,
timing_prompt_section=(
timing_response.prompt_section if timing_response is not None else None
),
planner_selected_history_count=(
response.selected_history_count if response is not None else None
),
planner_prompt_tokens=response.prompt_tokens if response is not None else None,
planner_response=response.content or "" if response is not None else "",
planner_tool_calls=response.tool_calls if response is not None else None,
planner_tool_results=tool_result_summaries,
planner_tool_detail_results=tool_monitor_results,
planner_prompt_section=response.prompt_section if response is not None else None,
)
await emit_planner_finalized(
session_id=self._runtime.session_id,
cycle_id=cycle_detail.cycle_id,

View File

@@ -58,6 +58,7 @@ class MaisakaHeartFlowChatting:
self.chat_stream: BotChatSession = chat_stream
session_name = chat_manager.get_session_name(session_id) or session_id
self.session_name = session_name
self.log_prefix = f"[{session_name}]"
self._chat_loop_service = MaisakaChatLoopService(
session_id=session_id,
@@ -692,28 +693,117 @@ class MaisakaHeartFlowChatting:
def _render_context_usage_panel(
self,
*,
selected_history_count: int,
prompt_tokens: int,
cycle_id: Optional[int] = None,
timing_selected_history_count: Optional[int] = None,
timing_prompt_tokens: Optional[int] = None,
timing_action: str = "",
timing_response: str = "",
timing_tool_calls: Optional[list[Any]] = None,
timing_tool_results: Optional[list[str]] = None,
timing_tool_detail_results: Optional[list[dict[str, Any]]] = None,
timing_prompt_section: Optional[RenderableType] = None,
planner_selected_history_count: Optional[int] = None,
planner_prompt_tokens: Optional[int] = None,
planner_response: str = "",
tool_calls: Optional[list[Any]] = None,
tool_results: Optional[list[str]] = None,
tool_detail_results: Optional[list[dict[str, Any]]] = None,
prompt_section: Optional[RenderableType] = None,
planner_tool_calls: Optional[list[Any]] = None,
planner_tool_results: Optional[list[str]] = None,
planner_tool_detail_results: Optional[list[dict[str, Any]]] = None,
planner_prompt_section: Optional[RenderableType] = None,
) -> None:
"""在终端展示当前聊天流的上下文占用、规划结果与工具结果。"""
"""在终端展示当前聊天流本轮 cycle 的最终结果。"""
if not global_config.debug.show_maisaka_thinking:
return
body_lines = [
f"上下文占用:{selected_history_count}/{self._max_context_size}",
f"本次请求token消耗{format_token_count(prompt_tokens)}",
f"聊天流名称:{getattr(self, 'session_name', self.session_id)}",
f"聊天流ID{self.session_id}",
]
if cycle_id is not None:
body_lines.append(f"循环编号:{cycle_id}")
renderables: list[RenderableType] = [Text("\n".join(body_lines))]
timing_panel = self._build_cycle_stage_panel(
title="Timing Gate",
border_style="bright_magenta",
selected_history_count=timing_selected_history_count,
prompt_tokens=timing_prompt_tokens,
response_text=timing_response,
tool_calls=timing_tool_calls,
tool_results=timing_tool_results,
tool_detail_results=timing_tool_detail_results,
prompt_section=timing_prompt_section,
extra_lines=[f"门控动作:{timing_action}"] if timing_action.strip() else None,
)
if timing_panel is not None:
renderables.append(timing_panel)
planner_panel = self._build_cycle_stage_panel(
title="Planner",
border_style="green",
selected_history_count=planner_selected_history_count,
prompt_tokens=planner_prompt_tokens,
response_text=planner_response,
tool_calls=planner_tool_calls,
tool_results=planner_tool_results,
tool_detail_results=planner_tool_detail_results,
prompt_section=planner_prompt_section,
)
if planner_panel is not None:
renderables.append(planner_panel)
console.print(
Panel(
Group(*renderables),
title="MaiSaka 循环",
border_style="bright_blue",
padding=(0, 1),
)
)
def _build_cycle_stage_panel(
self,
*,
title: str,
border_style: str,
selected_history_count: Optional[int],
prompt_tokens: Optional[int],
response_text: str = "",
tool_calls: Optional[list[Any]] = None,
tool_results: Optional[list[str]] = None,
tool_detail_results: Optional[list[dict[str, Any]]] = None,
prompt_section: Optional[RenderableType] = None,
extra_lines: Optional[list[str]] = None,
) -> Optional[Panel]:
"""构建单个 cycle 阶段的展示卡片。"""
has_content = any([
selected_history_count is not None,
prompt_tokens is not None,
bool(response_text.strip()),
bool(tool_calls),
bool(tool_results),
bool(tool_detail_results),
prompt_section is not None,
bool(extra_lines),
])
if not has_content:
return None
body_lines: list[str] = []
if selected_history_count is not None:
body_lines.append(f"上下文占用:{selected_history_count}/{self._max_context_size}")
if prompt_tokens is not None:
body_lines.append(f"本次请求token消耗{format_token_count(prompt_tokens)}")
if extra_lines:
body_lines.extend([line for line in extra_lines if isinstance(line, str) and line.strip()])
renderables: list[RenderableType] = []
if body_lines:
renderables.append(Text("\n".join(body_lines)))
if prompt_section is not None:
renderables.append(prompt_section)
normalized_response = planner_response.strip()
normalized_response = response_text.strip()
if normalized_response:
renderables.append(
Panel(
@@ -753,13 +843,11 @@ class MaisakaHeartFlowChatting:
if detail_panels:
renderables.extend(detail_panels)
console.print(
Panel(
Group(*renderables),
title="MaiSaka 上下文与结果",
border_style="bright_blue",
padding=(0, 1),
)
return Panel(
Group(*renderables),
title=title,
border_style=border_style,
padding=(0, 1),
)
@staticmethod

View File

@@ -63,7 +63,7 @@ class RuntimeDataCapabilityMixin:
@staticmethod
def _build_emoji_temp_path() -> Path:
from src.chat.emoji_system.emoji_manager import EMOJI_DIR
from src.emoji_system.emoji_manager import EMOJI_DIR
EMOJI_DIR.mkdir(parents=True, exist_ok=True)
return EMOJI_DIR / f"emoji_cap_{int(time.time() * 1000000)}.png"
@@ -463,7 +463,7 @@ class RuntimeDataCapabilityMixin:
return {"success": False, "error": str(e)}
async def _cap_emoji_get_by_description(self, plugin_id: str, capability: str, args: Dict[str, Any]) -> Any:
from src.chat.emoji_system.emoji_manager import emoji_manager
from src.emoji_system.emoji_manager import emoji_manager
description: str = args.get("description", "")
if not description:
@@ -485,7 +485,7 @@ class RuntimeDataCapabilityMixin:
return {"success": False, "error": str(e)}
async def _cap_emoji_get_random(self, plugin_id: str, capability: str, args: Dict[str, Any]) -> Any:
from src.chat.emoji_system.emoji_manager import emoji_manager
from src.emoji_system.emoji_manager import emoji_manager
count: int = args.get("count", 1)
try:
@@ -512,7 +512,7 @@ class RuntimeDataCapabilityMixin:
async def _cap_emoji_get_count(self, plugin_id: str, capability: str, args: Dict[str, Any]) -> Any:
try:
from src.chat.emoji_system.emoji_manager import emoji_manager
from src.emoji_system.emoji_manager import emoji_manager
return {"success": True, "count": len(emoji_manager.emojis)}
except Exception as e:
@@ -521,7 +521,7 @@ class RuntimeDataCapabilityMixin:
async def _cap_emoji_get_emotions(self, plugin_id: str, capability: str, args: Dict[str, Any]) -> Any:
try:
from src.chat.emoji_system.emoji_manager import emoji_manager
from src.emoji_system.emoji_manager import emoji_manager
emotions = sorted(
{
@@ -540,7 +540,7 @@ class RuntimeDataCapabilityMixin:
async def _cap_emoji_get_all(self, plugin_id: str, capability: str, args: Dict[str, Any]) -> Any:
try:
from src.chat.emoji_system.emoji_manager import emoji_manager
from src.emoji_system.emoji_manager import emoji_manager
emojis = []
for emoji in emoji_manager.emojis:
@@ -556,7 +556,7 @@ class RuntimeDataCapabilityMixin:
async def _cap_emoji_get_info(self, plugin_id: str, capability: str, args: Dict[str, Any]) -> Any:
try:
from src.chat.emoji_system.emoji_manager import emoji_manager
from src.emoji_system.emoji_manager import emoji_manager
from src.config.config import global_config
current_count = len(emoji_manager.emojis)
@@ -573,7 +573,7 @@ class RuntimeDataCapabilityMixin:
return {"success": False, "error": str(e)}
async def _cap_emoji_register(self, plugin_id: str, capability: str, args: Dict[str, Any]) -> Any:
from src.chat.emoji_system.emoji_manager import emoji_manager
from src.emoji_system.emoji_manager import emoji_manager
emoji_base64: str = args.get("emoji_base64", "")
if not emoji_base64:
@@ -630,7 +630,7 @@ class RuntimeDataCapabilityMixin:
return {"success": False, "error": str(e)}
async def _cap_emoji_delete(self, plugin_id: str, capability: str, args: Dict[str, Any]) -> Any:
from src.chat.emoji_system.emoji_manager import emoji_manager
from src.emoji_system.emoji_manager import emoji_manager
emoji_hash: str = args.get("emoji_hash", "")
if not emoji_hash:

View File

@@ -20,7 +20,7 @@ def _get_builtin_hook_spec_registrars() -> List[HookSpecRegistrar]:
"""
from src.chat.message_receive.bot import register_chat_hook_specs
from src.chat.emoji_system.emoji_manager import register_emoji_hook_specs
from src.emoji_system.emoji_manager import register_emoji_hook_specs
from src.learners.expression_learner import register_expression_hook_specs
from src.learners.jargon_miner import register_jargon_hook_specs
from src.maisaka.chat_loop_service import register_maisaka_hook_specs