From 5da65140200beef7864f86a17b8a21f08571a740 Mon Sep 17 00:00:00 2001 From: liuwei Date: Mon, 27 Apr 2026 09:16:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=A4=B4=E5=83=8F=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E6=97=A7=E6=96=87=E4=BB=B6=E6=B8=85=E7=90=86=E6=9C=BA?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- utils/wechat/contact_manager.py | 53 ++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/utils/wechat/contact_manager.py b/utils/wechat/contact_manager.py index be46186..4210a5d 100644 --- a/utils/wechat/contact_manager.py +++ b/utils/wechat/contact_manager.py @@ -147,6 +147,44 @@ class ContactManager: except Exception as exc: self._logger.warning(f"保存头像缓存索引失败: {exc}") + def _delete_avatar_file_by_name(self, file_name: str) -> None: + """按文件名删除缓存头像,删除失败时只记日志不打断主流程。""" + file_name = str(file_name or "").strip() + if not file_name or not self._avatar_cache_dir: + return + target_path = self._avatar_cache_dir / file_name + if not target_path.exists(): + return + try: + target_path.unlink() + except Exception as exc: + self._logger.warning(f"删除旧头像缓存失败 file={target_path}: {exc}") + + def _cleanup_avatar_cache_files(self) -> None: + """清理缓存目录里的孤儿头像文件与残留临时文件。""" + if not self._avatar_cache_dir: + return + + # 允许保留 manifest 自身,其余不在当前索引里的文件一律视为孤儿文件。 + referenced_files = { + str(meta.get("file_name") or "").strip() + for meta in self._avatar_manifest.values() + if str(meta.get("file_name") or "").strip() + } + manifest_name = self._avatar_manifest_path.name if self._avatar_manifest_path else "" + + for file_path in self._avatar_cache_dir.iterdir(): + if not file_path.is_file(): + continue + if file_path.name == manifest_name: + continue + # `.tmp` 文件说明上次下载未完整落盘,直接清掉,避免长时间堆积。 + if file_path.suffix.lower() == ".tmp": + self._delete_avatar_file_by_name(file_path.name) + continue + if file_path.name not in referenced_files: + self._delete_avatar_file_by_name(file_path.name) + def _guess_avatar_extension(self, avatar_url: str, content_type: str) -> str: """推断头像文件扩展名,尽量保留真实图片类型。""" guessed_from_type = mimetypes.guess_extension((content_type or "").split(";")[0].strip()) or "" @@ -211,6 +249,7 @@ class ContactManager: if not remote_url: continue manifest_item = self._avatar_manifest.get(wxid, {}) + old_file_name = str(manifest_item.get("file_name") or "").strip() cached_path = self.get_cached_head_image_path(wxid) # 只有“URL 变了”或“本地文件丢了”才重新下载,避免刷新通讯录时重复打远端。 if manifest_item.get("remote_url") == remote_url and cached_path: @@ -222,16 +261,23 @@ class ContactManager: "file_name": Path(downloaded_path).name, "remote_url": remote_url, } + # 同一联系人头像地址变化后,旧文件已经失去引用,这里立刻删掉旧版本。 + if old_file_name and old_file_name != Path(downloaded_path).name: + self._delete_avatar_file_by_name(old_file_name) manifest_changed = True # 把已经不在通讯录中的头像记录清理掉,避免 manifest 无限增长。 removed_wxids = [wxid for wxid in self._avatar_manifest.keys() if wxid not in self._head_images] for wxid in removed_wxids: - self._avatar_manifest.pop(wxid, None) + removed_meta = self._avatar_manifest.pop(wxid, None) or {} + self._delete_avatar_file_by_name(str(removed_meta.get("file_name") or "").strip()) manifest_changed = True if manifest_changed: self._save_avatar_manifest() + # 无论本轮 manifest 是否有变化,都顺手做一次目录对账, + # 保证历史异常中断留下的孤儿文件也能逐步被回收。 + self._cleanup_avatar_cache_files() def get_cached_head_image_path(self, wxid: str) -> str: """返回头像缓存本地路径,若缓存不存在则返回空字符串。""" @@ -258,6 +304,7 @@ class ContactManager: with self._avatar_cache_lock: # 双重检查避免高并发下重复下载同一张头像。 manifest_item = self._avatar_manifest.get(wxid) or {} + old_file_name = str(manifest_item.get("file_name") or "").strip() cached_path = self.get_cached_head_image_path(wxid) if cached_path and manifest_item.get("remote_url") == remote_url: return cached_path @@ -268,7 +315,11 @@ class ContactManager: "file_name": Path(downloaded_path).name, "remote_url": remote_url, } + # 单头像补下载时同样清掉旧版本,避免访问链路把目录越堆越大。 + if old_file_name and old_file_name != Path(downloaded_path).name: + self._delete_avatar_file_by_name(old_file_name) self._save_avatar_manifest() + self._cleanup_avatar_cache_files() return downloaded_path def get_head_image_version(self, wxid: str) -> str: