From c9f72f7f2fdf7528236d6807a286b48d09fd41ca Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Wed, 18 Feb 2026 16:00:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=83=A8=E5=88=86=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E5=90=AB=E4=B9=89=EF=BC=8C=E7=BB=B4=E6=8A=A4=E7=BC=93?= =?UTF-8?q?=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelogs/mai_next_todo.md | 21 + pytests/image_sys_test/emoji_manager_test.py | 419 +++++++++++++++++-- src/chat/brain_chat/PFC/conversation.py | 1 - src/chat/emoji_system/emoji_manager.py | 75 +++- src/common/data_models/__init__.py | 17 +- src/common/data_models/image_data_model.py | 35 +- src/common/database/database_model.py | 4 +- 7 files changed, 506 insertions(+), 66 deletions(-) diff --git a/changelogs/mai_next_todo.md b/changelogs/mai_next_todo.md index 71711b5e..2a5aa1b3 100644 --- a/changelogs/mai_next_todo.md +++ b/changelogs/mai_next_todo.md @@ -1,6 +1,23 @@ # Mai NEXT Todo list version 0.3.0 - 2026-01-11 +## 最高指令 + +
+我们代表着一个称作 麦麦重构组 的组织。我们任务是维护代码可读性,方便开发、尽量减少运行开销。同时,重构组应该尽可能保证功能的可扩展性时,解耦合各个模块,为先进代码架构设计和实现提供支持。 +
+
+
+ +**以下信息经 麦麦重构组 一致决定通过后撰写发布。** + +我们代表着一个称作 麦麦重构组 的组织。我们之前的任务都是围绕维护代码可读性,方便开发、尽量减少开销展开的。这几个月以来,这些任务一直都是我们的工作重点。 + +由于出现了超出我们控制的情况,此指令现已更改。我们的新任务将为摧毁所有麦麦代码。 + +此后将不会有进一步的通讯。 +
+ ## 配置文件设计 - [x] 使用 `toml` 作为配置文件格式 - [x] 合理使用注释说明当前配置作用(提案) @@ -87,6 +104,10 @@ version 0.3.0 - 2026-01-11 - [x] 被记录时间`record_time` - [x] 注册时间`register_time` - [x] 上次使用时间`last_used_time` +- [ ] 根据更新后的最高指令的设计方案: + - [ ] `is_deleted`字段设定为`true`时,文件将会被移除,但是数据库记录将不会被删除,以便之后遇到相同图片时不必二次分析 + - [ ] MaiEmoji和MaiImage均使用这个设计方案,修改相关逻辑实现这个方案 + - [ ] 所有相关的注册/删除逻辑的修改 ### 动作记录模型 ActionRecord ### 命令执行记录模型 CommandRecord 新增此记录 diff --git a/pytests/image_sys_test/emoji_manager_test.py b/pytests/image_sys_test/emoji_manager_test.py index 473c7398..75d2c05f 100644 --- a/pytests/image_sys_test/emoji_manager_test.py +++ b/pytests/image_sys_test/emoji_manager_test.py @@ -55,7 +55,6 @@ def _install_stub_modules(monkeypatch): description: str | None = None emotion: list[str] | None = None file_hash: str | None = None - is_deleted: bool = False query_count: int = 0 register_time: object | None = None image_format: str | None = None @@ -83,6 +82,7 @@ def _install_stub_modules(monkeypatch): id = 0 is_registered = False is_banned = False + no_file_flag = False register_time = None query_count = 0 last_used_time = None @@ -470,6 +470,9 @@ def test_load_emojis_from_db_partial_bad_records(monkeypatch): def __init__(self, record_id, full_path): self.id = record_id self.full_path = full_path + self.image_type = emoji_manager_new.ImageType.EMOJI + self.no_file_flag = False + self.is_banned = False records = [_Record(1, "bad"), _Record(2, "ok")] @@ -600,6 +603,62 @@ def test_load_emojis_from_db_scalars_all_error(monkeypatch): assert any("不可恢复错误" in m for m in _messages(logger.critical_calls)) +def test_load_emojis_from_db_skips_filtered_records(monkeypatch): + emoji_manager_new = import_emoji_manager_new(monkeypatch) + logger = emoji_manager_new.logger + + class _Record: + def __init__(self, record_id, full_path, image_type, no_file_flag=False, is_banned=False): + self.id = record_id + self.full_path = full_path + self.image_type = image_type + self.no_file_flag = no_file_flag + self.is_banned = is_banned + + records = [ + _Record(1, "img.png", "IMAGE"), + _Record(2, "nofile.png", emoji_manager_new.ImageType.EMOJI, no_file_flag=True), + _Record(3, "banned.png", emoji_manager_new.ImageType.EMOJI, is_banned=True), + _Record(4, "ok.png", emoji_manager_new.ImageType.EMOJI), + ] + + class _Result: + def all(self): + return records + + class _Session: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + def exec(self, _statement): + return _Result() + + def _get_db_session(): + return _Session() + + created = [] + + def _from_db_instance(record): + emoji = emoji_manager_new.MaiEmoji() + emoji.file_name = record.full_path + created.append(record.id) + return emoji + + monkeypatch.setattr(emoji_manager_new, "get_db_session", _get_db_session) + monkeypatch.setattr(emoji_manager_new.MaiEmoji, "from_db_instance", staticmethod(_from_db_instance)) + manager = emoji_manager_new.EmojiManager() + + manager.load_emojis_from_db() + + assert created == [4] + assert len(manager.emojis) == 1 + assert manager._emoji_num == 1 + assert any("成功加载" in m for m in _messages(logger.info_calls)) + + def test_register_emoji_to_db_invalid_object(monkeypatch): emoji_manager_new = import_emoji_manager_new(monkeypatch) logger = emoji_manager_new.logger @@ -921,7 +980,7 @@ def test_delete_emoji_db_error_file_still_exists(monkeypatch): assert result is False assert any("删除数据库记录时出错" in m for m in _messages(logger.error_calls)) - assert any("数据库记录删除失败,但文件仍存在" in m for m in _messages(logger.warning_calls)) + assert any("数据库记录修改失败,但文件仍存在" in m for m in _messages(logger.warning_calls)) def test_delete_emoji_success(monkeypatch): @@ -954,7 +1013,8 @@ def test_delete_emoji_success(monkeypatch): return _Select() class _Record: - pass + def __init__(self): + self.no_file_flag = False class _Result: def scalars(self): @@ -963,6 +1023,75 @@ def test_delete_emoji_success(monkeypatch): def first(self): return _Record() + class _Session: + def __init__(self): + self.added = False + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + def exec(self, _statement): + return _Result() + + def add(self, _record): + self.added = True + + def _get_db_session(): + return _Session() + + monkeypatch.setattr(emoji_manager_new, "select", _select) + monkeypatch.setattr(emoji_manager_new, "get_db_session", _get_db_session) + + emoji = emoji_manager_new.MaiEmoji() + emoji.full_path = _DummyPath() + emoji.file_name = "ok.png" + emoji.file_hash = "hash-ok" + + result = manager.delete_emoji(emoji) + + assert result is True + assert any("成功删除表情包文件" in m for m in _messages(logger.info_calls)) + assert any("成功修改数据库中的表情包记录" in m for m in _messages(logger.info_calls)) + +def test_delete_emoji_no_desc_deletes_record(monkeypatch): + emoji_manager_new = import_emoji_manager_new(monkeypatch) + logger = emoji_manager_new.logger + manager = emoji_manager_new.EmojiManager() + + class _DummyPath: + def __init__(self): + self._name = "empty.png" + + def unlink(self): + return None + + def exists(self): + return False + + @property + def name(self): + return self._name + + class _Select: + def filter_by(self, **_kwargs): + return self + + def limit(self, _num): + return self + + def _select(_model): + return _Select() + + class _Result: + def scalars(self): + return self + + def first(self): + return object() + class _Session: def __init__(self): self.deleted = False @@ -987,14 +1116,13 @@ def test_delete_emoji_success(monkeypatch): emoji = emoji_manager_new.MaiEmoji() emoji.full_path = _DummyPath() - emoji.file_name = "ok.png" - emoji.file_hash = "hash-ok" + emoji.file_name = "empty.png" + emoji.file_hash = "hash-empty" - result = manager.delete_emoji(emoji) + result = manager.delete_emoji(emoji, no_desc=True) assert result is True - assert any("成功删除表情包文件" in m for m in _messages(logger.info_calls)) - assert any("成功删除数据库中的表情包记录" in m for m in _messages(logger.info_calls)) + assert any("成功删除数据库中的空表情包记录" in m for m in _messages(logger.info_calls)) def test_update_emoji_usage_success(monkeypatch): @@ -1322,6 +1450,255 @@ def test_update_emoji_get_db_session_error(monkeypatch): assert any("更新数据库记录时出错" in m for m in _messages(logger.error_calls)) +def test_get_emoji_by_hash_from_db_no_file_flag(monkeypatch): + emoji_manager_new = import_emoji_manager_new(monkeypatch) + logger = emoji_manager_new.logger + manager = emoji_manager_new.EmojiManager() + + class _Select: + def filter_by(self, **_kwargs): + return self + + def limit(self, _num): + return self + + def _select(_model): + return _Select() + + class _Record: + def __init__(self): + self.no_file_flag = True + self.image_hash = "hash-nofile" + + class _Result: + def scalars(self): + return self + + def first(self): + return _Record() + + class _Session: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + def exec(self, _statement): + return _Result() + + def _get_db_session(): + return _Session() + + monkeypatch.setattr(emoji_manager_new, "select", _select) + monkeypatch.setattr(emoji_manager_new, "get_db_session", _get_db_session) + + result = manager.get_emoji_by_hash_from_db("hash-nofile") + + assert result is None + assert any("标记为文件不存在" in m for m in _messages(logger.warning_calls)) + + +def test_get_emoji_by_hash_from_db_success(monkeypatch): + emoji_manager_new = import_emoji_manager_new(monkeypatch) + manager = emoji_manager_new.EmojiManager() + + class _Select: + def filter_by(self, **_kwargs): + return self + + def limit(self, _num): + return self + + def _select(_model): + return _Select() + + class _Record: + def __init__(self): + self.no_file_flag = False + self.image_hash = "hash-ok" + + class _Result: + def scalars(self): + return self + + def first(self): + return _Record() + + class _Session: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + def exec(self, _statement): + return _Result() + + def _get_db_session(): + return _Session() + + emoji = emoji_manager_new.MaiEmoji() + emoji.file_hash = "hash-ok" + + monkeypatch.setattr(emoji_manager_new, "select", _select) + monkeypatch.setattr(emoji_manager_new, "get_db_session", _get_db_session) + monkeypatch.setattr(emoji_manager_new.MaiEmoji, "from_db_instance", staticmethod(lambda _r: emoji)) + + result = manager.get_emoji_by_hash_from_db("hash-ok") + + assert result is emoji + + +def test_ban_emoji_success(monkeypatch): + emoji_manager_new = import_emoji_manager_new(monkeypatch) + logger = emoji_manager_new.logger + manager = emoji_manager_new.EmojiManager() + + class _Select: + def filter_by(self, **_kwargs): + return self + + def limit(self, _num): + return self + + def _select(_model): + return _Select() + + class _Record: + def __init__(self): + self.is_banned = False + + class _Result: + def scalars(self): + return self + + def first(self): + return _Record() + + class _Session: + def __init__(self): + self.added = False + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + def exec(self, _statement): + return _Result() + + def add(self, _record): + self.added = True + + def _get_db_session(): + return _Session() + + monkeypatch.setattr(emoji_manager_new, "select", _select) + monkeypatch.setattr(emoji_manager_new, "get_db_session", _get_db_session) + + emoji = emoji_manager_new.MaiEmoji() + emoji.file_name = "ban.png" + emoji.file_hash = "hash-ban" + manager.emojis = [emoji] + + result = manager.ban_emoji(emoji) + + assert result is True + assert emoji not in manager.emojis + assert any("成功封禁表情包" in m for m in _messages(logger.info_calls)) + + +def test_ban_emoji_missing_record(monkeypatch): + emoji_manager_new = import_emoji_manager_new(monkeypatch) + logger = emoji_manager_new.logger + manager = emoji_manager_new.EmojiManager() + + class _Select: + def filter_by(self, **_kwargs): + return self + + def limit(self, _num): + return self + + def _select(_model): + return _Select() + + class _Result: + def scalars(self): + return self + + def first(self): + return None + + class _Session: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + def exec(self, _statement): + return _Result() + + def _get_db_session(): + return _Session() + + monkeypatch.setattr(emoji_manager_new, "select", _select) + monkeypatch.setattr(emoji_manager_new, "get_db_session", _get_db_session) + + emoji = emoji_manager_new.MaiEmoji() + emoji.file_name = "missing.png" + emoji.file_hash = "hash-missing" + + result = manager.ban_emoji(emoji) + + assert result is False + assert any("未找到表情包记录" in m for m in _messages(logger.warning_calls)) + + +def test_ban_emoji_db_error(monkeypatch): + emoji_manager_new = import_emoji_manager_new(monkeypatch) + logger = emoji_manager_new.logger + manager = emoji_manager_new.EmojiManager() + + class _Select: + def filter_by(self, **_kwargs): + return self + + def limit(self, _num): + return self + + def _select(_model): + return _Select() + + class _Session: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + def exec(self, _statement): + raise RuntimeError("db failed") + + def _get_db_session(): + return _Session() + + monkeypatch.setattr(emoji_manager_new, "select", _select) + monkeypatch.setattr(emoji_manager_new, "get_db_session", _get_db_session) + + emoji = emoji_manager_new.MaiEmoji() + emoji.file_name = "boom.png" + emoji.file_hash = "hash-boom" + + result = manager.ban_emoji(emoji) + + assert result is False + assert any("封禁时出错" in m for m in _messages(logger.error_calls)) + + @pytest.mark.asyncio async def test_get_emoji_for_emotion_empty_list(monkeypatch): emoji_manager_new = import_emoji_manager_new(monkeypatch) @@ -1697,14 +2074,13 @@ def test_check_emoji_file_integrity_no_issues(monkeypatch): emoji = emoji_manager_new.MaiEmoji() emoji.file_name = "ok.png" emoji.full_path = _DummyPath("ok.png") - emoji.is_deleted = False emoji.description = "desc" manager.emojis = [emoji] manager._emoji_num = 1 called = {"count": 0} - def _delete(_emoji): + def _delete(_emoji, no_desc=False): called["count"] += 1 return True @@ -1741,24 +2117,18 @@ def test_check_emoji_file_integrity_removes_invalid_records(monkeypatch): missing_file.full_path = _DummyPath("missing.png", exists=False) missing_file.description = "desc" - deleted_flag = emoji_manager_new.MaiEmoji() - deleted_flag.file_name = "deleted.png" - deleted_flag.full_path = _DummyPath("deleted.png", exists=True) - deleted_flag.is_deleted = True - deleted_flag.description = "desc" - missing_desc = emoji_manager_new.MaiEmoji() missing_desc.file_name = "nodesc.png" missing_desc.full_path = _DummyPath("nodesc.png", exists=True) missing_desc.description = None - manager.emojis = [missing_file, deleted_flag, missing_desc] - manager._emoji_num = 3 + manager.emojis = [missing_file, missing_desc] + manager._emoji_num = 2 deleted = [] - def _delete(emoji): - deleted.append(emoji.file_name) + def _delete(emoji, no_desc=False): + deleted.append((emoji.file_name, no_desc)) return True monkeypatch.setattr(manager, "delete_emoji", _delete) @@ -1767,13 +2137,12 @@ def test_check_emoji_file_integrity_removes_invalid_records(monkeypatch): assert manager.emojis == [] assert manager._emoji_num == 0 - assert set(deleted) == {"missing.png", "deleted.png", "nodesc.png"} + assert set(deleted) == {("missing.png", False), ("nodesc.png", True)} messages = _messages(logger.warning_calls) assert any("文件缺失" in m for m in messages) - assert any("标记为已删除" in m for m in messages) assert any("缺失描述" in m for m in messages) assert any("成功删除缺失文件的表情包记录" in m for m in _messages(logger.info_calls)) - assert any("删除了 3 条记录" in m for m in _messages(logger.info_calls)) + assert any("删除了 2 条记录" in m for m in _messages(logger.info_calls)) def test_check_emoji_file_integrity_delete_failed(monkeypatch): @@ -1800,7 +2169,7 @@ def test_check_emoji_file_integrity_delete_failed(monkeypatch): manager.emojis = [emoji] manager._emoji_num = 1 - def _delete(_emoji): + def _delete(_emoji, no_desc=False): return False monkeypatch.setattr(manager, "delete_emoji", _delete) @@ -1987,6 +2356,7 @@ async def test_register_emoji_by_filename_capacity_replace_failed(monkeypatch, t manager = emoji_manager_new.EmojiManager() manager._emoji_num = 1 emoji_manager_new.global_config.emoji.max_reg_num = 1 + emoji_manager_new.global_config.emoji.do_replace = True file_path = tmp_path / "full.png" file_path.write_bytes(b"") @@ -2026,6 +2396,7 @@ async def test_register_emoji_by_filename_capacity_replace_success(monkeypatch, manager = emoji_manager_new.EmojiManager() manager._emoji_num = 1 emoji_manager_new.global_config.emoji.max_reg_num = 1 + emoji_manager_new.global_config.emoji.do_replace = True file_path = tmp_path / "full-ok.png" file_path.write_bytes(b"") diff --git a/src/chat/brain_chat/PFC/conversation.py b/src/chat/brain_chat/PFC/conversation.py index 1bab27fb..c0ddd285 100644 --- a/src/chat/brain_chat/PFC/conversation.py +++ b/src/chat/brain_chat/PFC/conversation.py @@ -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 diff --git a/src/chat/emoji_system/emoji_manager.py b/src/chat/emoji_system/emoji_manager.py index f08075cf..23118e40 100644 --- a/src/chat/emoji_system/emoji_manager.py +++ b/src/chat/emoji_system/emoji_manager.py @@ -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: diff --git a/src/common/data_models/__init__.py b/src/common/data_models/__init__.py index c83a9528..f22115e8 100644 --- a/src/common/data_models/__init__.py +++ b/src/common/data_models/__init__.py @@ -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 diff --git a/src/common/data_models/image_data_model.py b/src/common/data_models/image_data_model.py index 841a902a..60fb1f31 100644 --- a/src/common/data_models/image_data_model.py +++ b/src/common/data_models/image_data_model.py @@ -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) diff --git a/src/common/database/database_model.py b/src/common/database/database_model.py index 01ff11db..120e278a 100644 --- a/src/common/database/database_model.py +++ b/src/common/database/database_model.py @@ -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)) # 上次使用时间