修改部分字段含义,维护缓存

This commit is contained in:
UnCLAS-Prommer
2026-02-18 16:00:45 +08:00
parent 545e3b4982
commit c9f72f7f2f
7 changed files with 506 additions and 66 deletions

View File

@@ -4,7 +4,6 @@ import datetime
# from .message_storage import MongoDBMessageStorage
from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
from src.common.data_models import transform_class_to_dict
# from src.config.config import global_config
from typing import Dict, Any, Optional

View File

@@ -64,6 +64,12 @@ class EmojiManager:
statement = select(Images)
results = session.exec(statement).all()
for record in results:
if record.image_type != ImageType.EMOJI:
continue
if record.no_file_flag:
continue
if record.is_banned:
continue
try:
emoji = MaiEmoji.from_db_instance(record)
self.emojis.append(emoji)
@@ -121,12 +127,13 @@ class EmojiManager:
return False
return True
def delete_emoji(self, emoji: MaiEmoji) -> bool:
def delete_emoji(self, emoji: MaiEmoji, no_desc: bool = False) -> bool:
"""
删除表情包的文件和数据库记录
Args:
emoji (MaiEmoji): 需要删除的表情包对象
no_desc (bool): 如果为 True则表示删除的表情包记录没有描述信息删除时直接删除数据库记录如果为`False`,则表示删除的表情包记录有描述信息,删除时将数据库记录的`no_file_flag`标记为`True`而不是直接删除记录。默认为`False`。
Returns:
return (bool): 删除是否成功
"""
@@ -146,15 +153,20 @@ class EmojiManager:
with get_db_session() as session:
statement = select(Images).filter_by(image_hash=emoji.file_hash, image_type=ImageType.EMOJI).limit(1)
if image_record := session.exec(statement).first():
session.delete(image_record)
logger.info(f"[删除表情包] 成功删除数据库中的表情包记录: {emoji.file_hash}")
if no_desc:
session.delete(image_record)
logger.info(f"[删除表情包] 成功删除数据库中的空表情包记录: {emoji.file_name}")
else:
image_record.no_file_flag = True
session.add(image_record)
logger.info(f"[删除表情包] 成功修改数据库中的表情包记录: {emoji.file_name}")
else:
logger.warning(f"[删除表情包] 数据库中未找到表情包记录: {emoji.file_hash}")
logger.warning(f"[删除表情包] 数据库中未找到表情包记录: {emoji.file_name}")
except Exception as e:
logger.error(f"[删除表情包] 删除数据库记录时出错: {e}")
# 如果数据库记录删除失败,但文件可能已删除,记录一个警告
if file_to_delete.exists():
logger.warning(f"[删除表情包] 数据库记录删除失败,但文件仍存在: {emoji.file_name}")
logger.warning(f"[删除表情包] 数据库记录修改失败,但文件仍存在: {emoji.file_name}")
return False
return True
@@ -177,7 +189,7 @@ class EmojiManager:
statement = select(Images).filter_by(image_hash=emoji.file_hash, image_type=ImageType.EMOJI).limit(1)
if image_record := session.exec(statement).first():
emoji.query_count += 1
image_record.query_count= emoji.query_count
image_record.query_count = emoji.query_count
emoji.last_used_time = datetime.now()
image_record.last_used_time = emoji.last_used_time
session.add(image_record)
@@ -229,7 +241,7 @@ class EmojiManager:
return (Optional[MaiEmoji]): 返回表情包对象,如果未找到则返回 None
"""
for emoji in self.emojis:
if emoji.file_hash == emoji_hash and not emoji.is_deleted:
if emoji.file_hash == emoji_hash:
return emoji
logger.info(f"[获取表情包] 未找到哈希值为 {emoji_hash} 的表情包")
return None
@@ -245,8 +257,15 @@ class EmojiManager:
"""
try:
with get_db_session() as session:
statement = select(Images).filter_by(image_hash=emoji_hash, image_type=ImageType.EMOJI).limit(1)
statement = (
select(Images)
.filter_by(image_hash=emoji_hash, image_type=ImageType.EMOJI, is_banned=False)
.limit(1)
)
if image_record := session.exec(statement).first():
if image_record.no_file_flag:
logger.warning(f"[数据库] 表情包记录 {emoji_hash} 标记为文件不存在,无法获取表情包对象")
return None
return MaiEmoji.from_db_instance(image_record)
logger.info(f"[数据库] 未找到哈希值为 {emoji_hash} 的表情包记录")
return None
@@ -254,6 +273,25 @@ class EmojiManager:
logger.error(f"[数据库] 获取表情包时出错: {e}")
return None
def ban_emoji(self, emoji: MaiEmoji) -> bool:
"""封禁表情包,将表情包的 is_banned 字段设置为 True并从表情包列表中移除"""
try:
with get_db_session() as session:
statement = select(Images).filter_by(image_hash=emoji.file_hash, image_type=ImageType.EMOJI).limit(1)
if image_record := session.exec(statement).first():
image_record.is_banned = True
session.add(image_record)
if emoji in self.emojis:
self.emojis.remove(emoji)
logger.info(f"[封禁表情包] 成功封禁表情包: {emoji.file_name}")
else:
logger.warning(f"[封禁表情包] 未找到表情包记录: {emoji.file_name}")
return False
except Exception as e:
logger.error(f"[封禁表情包] 封禁时出错: {e}")
return False
return True
async def get_emoji_for_emotion(self, emotion_label: str) -> Optional[MaiEmoji]:
"""
根据文本情感标签获取合适的表情包
@@ -285,6 +323,8 @@ class EmojiManager:
"""
使用 LLM 决策替换一个表情包
**不校验是否开启表情包偷取**
Args:
new_emoji (MaiEmoji): 新添加的表情包对象
Returns:
@@ -361,7 +401,7 @@ class EmojiManager:
# 调用VLM生成描述
image_format = target_emoji.image_format
image_bytes = target_emoji.read_image_bytes(target_emoji.full_path)
image_bytes = await asyncio.to_thread(target_emoji.read_image_bytes, target_emoji.full_path)
if image_format == "gif":
try:
@@ -436,21 +476,18 @@ class EmojiManager:
检查表情包完整性,删除文件缺失的表情包记录
"""
logger.info("[完整性检查] 开始检查表情包文件完整性...")
to_delete_emojis: list[MaiEmoji] = []
to_delete_emojis: list[Tuple[MaiEmoji, bool]] = []
removal_count = 0
for emoji in self.emojis:
if not emoji.full_path.exists():
logger.warning(f"[完整性检查] 表情包文件缺失,准备删除记录: {emoji.file_name}")
to_delete_emojis.append(emoji)
if emoji.is_deleted:
logger.warning(f"[完整性检查] 表情包记录标记为已删除,准备删除记录: {emoji.file_name}")
to_delete_emojis.append(emoji)
logger.warning(f"[完整性检查] 表情包文件缺失,准备修改记录: {emoji.file_name}")
to_delete_emojis.append((emoji, False))
if not emoji.description:
logger.warning(f"[完整性检查] 表情包记录缺失描述,准备删除记录: {emoji.file_name}")
to_delete_emojis.append(emoji)
to_delete_emojis.append((emoji, True))
for emoji in to_delete_emojis:
if self.delete_emoji(emoji):
for emoji, is_description_empty in to_delete_emojis:
if self.delete_emoji(emoji, is_description_empty):
self.emojis.remove(emoji)
self._emoji_num -= 1
removal_count += 1
@@ -552,7 +589,7 @@ class EmojiManager:
return False
# 5. 检查容量并决定是否替换或者直接注册
if self._emoji_num >= global_config.emoji.max_reg_num:
if self._emoji_num >= global_config.emoji.max_reg_num and global_config.emoji.do_replace:
logger.warning(f"[注册表情包] 表情包数量已达上限{global_config.emoji.max_reg_num},尝试替换一个表情包")
replaced = await self.replace_an_emoji_by_llm(target_emoji)
if not replaced:

View File

@@ -1,7 +1,6 @@
from abc import ABC, abstractmethod
from dataclasses import is_dataclass
from typing import Any, Dict, Self, TypeVar, Generic, TYPE_CHECKING
from typing import Self, TypeVar, Generic, TYPE_CHECKING
import copy
@@ -16,20 +15,6 @@ class BaseDataModel:
return copy.deepcopy(self)
def transform_class_to_dict(obj: Any) -> Dict[str, Any]:
if obj is None:
return {}
if is_dataclass(obj):
return obj.__dict__
if hasattr(obj, "dict"):
return obj.dict()
if hasattr(obj, "model_dump"):
return obj.model_dump()
if hasattr(obj, "__dict__"):
return obj.__dict__
return {"value": obj}
class BaseDatabaseDataModel(ABC, Generic[T]):
@classmethod
@abstractmethod

View File

@@ -33,9 +33,8 @@ class BaseImageDataModel(BaseDatabaseDataModel[Images]):
self.file_hash: str = None # type: ignore
self.image_bytes: Optional[bytes] = image_bytes
self.image_format: str = "" # 图片格式
self.is_deleted: bool = False # 是否已被标记为删除
def read_image_bytes(self, path: Path) -> bytes:
"""
@@ -89,7 +88,6 @@ class BaseImageDataModel(BaseDatabaseDataModel[Images]):
Returns:
return (bool): 如果成功计算哈希值和格式则返回True否则返回False
"""
try:
# 计算哈希值
logger.debug(f"[初始化] 计算 {self.file_name} 的哈希值...")
@@ -109,7 +107,9 @@ class BaseImageDataModel(BaseDatabaseDataModel[Images]):
# 比对文件扩展名和实际格式
file_ext = self.file_name.split(".")[-1].lower()
if file_ext != self.image_format:
logger.warning(f"[初始化] {self.file_name} 文件扩展名与实际格式不符: ext`{file_ext}`!=`{self.image_format}`")
logger.warning(
f"[初始化] {self.file_name} 文件扩展名与实际格式不符: ext`{file_ext}`!=`{self.image_format}`"
)
# 重命名文件以匹配实际格式
new_file_name = ".".join(self.file_name.split(".")[:-1] + [self.image_format])
new_full_path = self.dir_path / new_file_name
@@ -120,7 +120,6 @@ class BaseImageDataModel(BaseDatabaseDataModel[Images]):
except Exception as e:
logger.error(f"[初始化] 初始化图片时发生错误: {e}")
logger.error(traceback.format_exc())
self.is_deleted = True
return False
@@ -136,6 +135,19 @@ class MaiEmoji(BaseImageDataModel):
@classmethod
def from_db_instance(cls, db_record: Images):
"""从数据库记录创建 MaiEmoji 对象,如果记录标记为文件不存在则**抛出异常**
调用者应该对数据库记录进行检查,如果 `no_file_flag` 为 True 则不应该调用此方法
Args:
db_record (Images): 数据库中的图片记录
Returns:
return (MaiEmoji): 包含图片信息的 MaiEmoji 对象
Raises:
ValueError: 如果数据库记录标记为文件不存在则抛出该异常
"""
if db_record.no_file_flag:
raise ValueError(f"数据库记录 {db_record.image_hash} 标记为文件不存在,无法创建 MaiEmoji 对象")
obj = cls(db_record.full_path)
obj.file_hash = db_record.image_hash
obj.description = db_record.description
@@ -168,6 +180,19 @@ class MaiImage(BaseImageDataModel):
@classmethod
def from_db_instance(cls, db_record: Images):
"""从数据库记录创建 MaiImage 对象,如果记录标记为文件不存在则**抛出异常**
调用者应该对数据库记录进行检查,如果 `no_file_flag` 为 True 则不应该调用此方法
Args:
db_record (Images): 数据库中的图片记录
Returns:
return (MaiImage): 包含图片信息的 MaiImage 对象
Raises:
ValueError: 如果数据库记录标记为文件不存在则抛出该异常
"""
if db_record.no_file_flag:
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)

View File

@@ -93,8 +93,10 @@ class Images(SQLModel, table=True):
query_count: int = Field(default=0) # 被查询次数
is_registered: bool = Field(default=False) # 是否已经注册
is_banned: bool = Field(default=False) # 被手动禁用
no_file_flag: bool = Field(default=False) # 文件不存在标记如果为True表示文件已经不存在仅保留描述字段
record_time: datetime = Field(default_factory=datetime.now, sa_column=Column(DateTime, index=True)) # 记录时间(被创建的时间)
record_time: datetime = Field(default_factory=datetime.now, sa_column=Column(DateTime, index=True)) # 记录时间(数据库记录被创建的时间)
register_time: Optional[datetime] = Field(default=None, sa_column=Column(DateTime, nullable=True)) # 注册时间(被注册为可用表情包的时间)
last_used_time: Optional[datetime] = Field(default=None, sa_column=Column(DateTime, nullable=True)) # 上次使用时间