fix:表情包识别失败问题
This commit is contained in:
@@ -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
|
||||
|
||||
# 导出主要组件供外部使用
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user