138ec9fa by 柴进

:bug: 修复历史记录累积后 refresh_history 触发 macOS SIGKILL

根因:
- refresh_history 每次调用都 history_list.clear() + 重建全部条目
- 每条加载 2K PNG 原图做 QPixmap (~16MB 解码) 再缩到 120x120
- 106 条历史 × 16MB = ~1.7GB 瞬时内存峰值
- macOS 内存压力触发 SIGKILL,不可被 faulthandler 捕获
- 日志里只有 "开始刷新" 没有 "加载到 N 条",确认死在 clear()/load 之间

修复:
- HistoryManager 新增 thumb_path_for + get_or_create_thumbnail:
  用 PIL 生成 240x240 JPEG 缓存到 <record_dir>/thumb.jpg,
  基于 mtime 判断是否重新生成
- save_generation 保存原图后顺便生成缩略图 (失败不影响主流程)
- refresh_history 只加载 thumb.jpg, 内存峰值从 1.7GB → 6MB (280x 下降)
- 缩略图生成失败用占位图兜底, 不回退加载原图 (防回到危险路径)
- 首次升级会为 106 条存量一次性补生成 thumb.jpg (PIL 串行 ~32MB 峰值, 安全)

附加:
- refresh_history 增加 "刷新完成" 收尾日志, 便于下次若再崩定位死亡位置

跨平台: 纯 PIL + pathlib + Qt 标准 API, Windows 零回归

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2473f5d4
...@@ -673,11 +673,53 @@ class HistoryManager: ...@@ -673,11 +673,53 @@ class HistoryManager:
673 673
674 self._update_history_index(history_item) 674 self._update_history_index(history_item)
675 675
676 # 生成缩略图(失败不影响主流程)
677 try:
678 self.get_or_create_thumbnail(generated_image_path)
679 except Exception as e:
680 self.logger.warning(f"保存历史记录时生成缩略图失败: {e}")
681
676 # 清理旧记录 682 # 清理旧记录
677 self._cleanup_old_records() 683 self._cleanup_old_records()
678 684
679 return timestamp 685 return timestamp
680 686
687 @staticmethod
688 def thumb_path_for(generated_image_path: Path) -> Path:
689 """缩略图在 generated.png 同目录下的固定名称 thumb.jpg。"""
690 return generated_image_path.parent / "thumb.jpg"
691
692 def get_or_create_thumbnail(self, generated_image_path: Path, size: int = 240):
693 """返回缓存缩略图路径,缺失或过期时用 PIL 生成一次。
694
695 refresh_history 每次加载 100+ 条 2K PNG 会导致 ~1.7GB 瞬时内存,
696 在 macOS 上触发 SIGKILL。缩略图把单条开销从 ~16MB 降到 ~5KB。
697
698 失败时返回 None,调用方应回退到原图或占位图。
699 """
700 try:
701 thumb_path = self.thumb_path_for(generated_image_path)
702 # 已缓存且不比源图旧 -> 直接用
703 if thumb_path.exists():
704 try:
705 if thumb_path.stat().st_mtime >= generated_image_path.stat().st_mtime:
706 return thumb_path
707 except OSError:
708 pass # stat 失败则重新生成
709 # 用 PIL 生成缩略图(比 QPixmap 省内存,且释放确定性)
710 from PIL import Image
711 with Image.open(generated_image_path) as img:
712 img = img.convert("RGB")
713 img.thumbnail((size, size), Image.LANCZOS)
714 img.save(str(thumb_path), "JPEG", quality=75, optimize=True)
715 return thumb_path
716 except Exception as e:
717 try:
718 self.logger.warning(f"生成缩略图失败 {generated_image_path}: {e}")
719 except Exception:
720 pass
721 return None
722
681 def _fix_history_path(self, stored_path: Path, timestamp: str) -> Path: 723 def _fix_history_path(self, stored_path: Path, timestamp: str) -> Path:
682 """修正历史记录中的路径,使其指向当前 base_path 724 """修正历史记录中的路径,使其指向当前 base_path
683 725
...@@ -2890,18 +2932,21 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2890,18 +2932,21 @@ class ImageGeneratorWindow(QMainWindow):
2890 tooltip += f"尺寸: {item.image_size}" 2932 tooltip += f"尺寸: {item.image_size}"
2891 list_item.setToolTip(tooltip) 2933 list_item.setToolTip(tooltip)
2892 2934
2893 # Try to load thumbnail 2935 # 加载缩略图:只走磁盘缓存的 thumb.jpg,避免每次解码 2K 原图
2936 # (2K PNG 解码 ~16MB × 100 条 ~1.7GB 瞬时内存 → macOS 上触发 SIGKILL)
2937 # 如果缩略图生成失败(极罕见)用占位图兜底,绝不回退去加载原图。
2894 if item.generated_image_path.exists(): 2938 if item.generated_image_path.exists():
2895 try: 2939 try:
2896 self.logger.debug(f"[refresh_history] 加载缩略图: {item.generated_image_path}") 2940 thumb_path = self.history_manager.get_or_create_thumbnail(item.generated_image_path)
2897 pixmap = QPixmap(str(item.generated_image_path)) 2941 if thumb_path is None:
2942 list_item.setIcon(self.create_placeholder_icon("缩略图\n失败"))
2943 else:
2944 pixmap = QPixmap(str(thumb_path))
2898 if not pixmap.isNull(): 2945 if not pixmap.isNull():
2899 # Scale to thumbnail size
2900 scaled_pixmap = pixmap.scaled(120, 120, Qt.KeepAspectRatio, Qt.SmoothTransformation) 2946 scaled_pixmap = pixmap.scaled(120, 120, Qt.KeepAspectRatio, Qt.SmoothTransformation)
2901 list_item.setIcon(QIcon(scaled_pixmap)) 2947 list_item.setIcon(QIcon(scaled_pixmap))
2902 else: 2948 else:
2903 self.logger.warning(f"[refresh_history] 缩略图加载为空: {item.generated_image_path}") 2949 self.logger.warning(f"[refresh_history] 缩略图加载为空: {thumb_path}")
2904 # Create placeholder icon
2905 list_item.setIcon(self.create_placeholder_icon("图片\n加载失败")) 2950 list_item.setIcon(self.create_placeholder_icon("图片\n加载失败"))
2906 except Exception as e: 2951 except Exception as e:
2907 self.logger.error(f"[refresh_history] 缩略图加载异常 {item.timestamp}: {e}") 2952 self.logger.error(f"[refresh_history] 缩略图加载异常 {item.timestamp}: {e}")
...@@ -2925,6 +2970,8 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2925,6 +2970,8 @@ class ImageGeneratorWindow(QMainWindow):
2925 if not history_items: 2970 if not history_items:
2926 self.clear_details_panel() 2971 self.clear_details_panel()
2927 2972
2973 self.logger.info(f"[refresh_history] 刷新完成,共渲染 {len(history_items)} 条")
2974
2928 def create_placeholder_icon(self, text): 2975 def create_placeholder_icon(self, text):
2929 """Create a placeholder icon with text""" 2976 """Create a placeholder icon with text"""
2930 # Create a 120x120 pixmap 2977 # Create a 120x120 pixmap
......