2473f5d4 by 柴进

:bug: 修复 macOS 粘贴闪退/长时间运行闪退 + :sparkles: 新增缩略图拖拽重排

粘贴闪退 (macOS 26):
- _safe_get_clipboard_image 在 Darwin 上禁用 mimeData.imageData() /
  clipboard.image() / application/x-qt-image 三条 native crash 路径,
  统一走 image/* 原始字节 + osascript PNGf 兜底
- DragDropScrollArea.dropEvent 的拖入图像分支同步做平台分流
- Windows/Linux 路径完全保留,零回归

长时间运行闪退:
- init_logging 改用 RotatingFileHandler (5MB × 5),避免日志无限增长
- 启动时清理超过 24 小时的 clipboard_*.png 遗留临时文件

Gemini 返回空图片:
- response_modalities 加上 TEXT,允许模型回传拒绝理由
- response.parts 增加 None 保护,修复日志里 20+ 次
  'NoneType object is not iterable' 异常
- 错误上浮 finish_reason + 模型说明到 QMessageBox

缩略图拖拽重排:
- 新增 DraggableThumbnail + THUMB_REORDER_MIME 内部协议
- 缩略图可拖动调整顺序,reorder_image 正确处理左右移动的索引

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f7fd7ef0
...@@ -13,7 +13,7 @@ from PySide6.QtWidgets import ( ...@@ -13,7 +13,7 @@ from PySide6.QtWidgets import (
13 QMenu, QProgressBar, QInputDialog 13 QMenu, QProgressBar, QInputDialog
14 ) 14 )
15 from PySide6.QtCore import Qt, QThread, Signal, QSize, QTimer, QMimeData 15 from PySide6.QtCore import Qt, QThread, Signal, QSize, QTimer, QMimeData
16 from PySide6.QtGui import QPixmap, QFont, QIcon, QDesktopServices, QAction, QImage, QDragEnterEvent, QDropEvent 16 from PySide6.QtGui import QPixmap, QFont, QIcon, QDesktopServices, QAction, QImage, QDragEnterEvent, QDropEvent, QDrag, QMouseEvent
17 from PySide6.QtCore import QUrl 17 from PySide6.QtCore import QUrl
18 18
19 import base64 19 import base64
...@@ -41,6 +41,36 @@ from dataclasses import dataclass, asdict ...@@ -41,6 +41,36 @@ from dataclasses import dataclass, asdict
41 from typing import List, Optional, Dict, Any 41 from typing import List, Optional, Dict, Any
42 42
43 43
44 def _cleanup_clipboard_tempfiles(max_age_hours: int = 24) -> int:
45 """清理遗留的剪贴板临时文件,防止长时间运行累积到磁盘/句柄上限。
46
47 仅删除 {tempdir}/nano_banana_app/clipboard_*.png / _clipboard_tmp.png 中
48 超过 max_age_hours 小时的文件。返回删除数量。
49 """
50 try:
51 import time
52 temp_dir = Path(tempfile.gettempdir()) / "nano_banana_app"
53 if not temp_dir.exists():
54 return 0
55 cutoff = time.time() - max_age_hours * 3600
56 removed = 0
57 for p in temp_dir.iterdir():
58 try:
59 if not p.is_file():
60 continue
61 name = p.name
62 if not (name.startswith("clipboard_") or name == "_clipboard_tmp.png"):
63 continue
64 if p.stat().st_mtime < cutoff:
65 p.unlink()
66 removed += 1
67 except Exception:
68 pass
69 return removed
70 except Exception:
71 return 0
72
73
44 def _get_crash_log_path() -> Path: 74 def _get_crash_log_path() -> Path:
45 """获取崩溃日志文件路径(尽早可用,不依赖任何初始化)""" 75 """获取崩溃日志文件路径(尽早可用,不依赖任何初始化)"""
46 system = platform.system() 76 system = platform.system()
...@@ -271,8 +301,12 @@ def init_logging(log_level=logging.INFO): ...@@ -271,8 +301,12 @@ def init_logging(log_level=logging.INFO):
271 # 配置日志格式 301 # 配置日志格式
272 log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 302 log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
273 303
274 # 配置处理器 304 # 配置处理器 - 使用 RotatingFileHandler 避免日志无限增长导致磁盘/IO 压力
275 handlers = [logging.FileHandler(log_file, encoding='utf-8')] 305 from logging.handlers import RotatingFileHandler
306 max_bytes = int(logging_config.get("max_bytes", 5 * 1024 * 1024)) # 默认 5MB
307 backup_count = int(logging_config.get("backup_count", 5))
308 handlers = [RotatingFileHandler(log_file, maxBytes=max_bytes,
309 backupCount=backup_count, encoding='utf-8')]
276 if logging_config.get("log_to_console", True): 310 if logging_config.get("log_to_console", True):
277 handlers.append(logging.StreamHandler()) 311 handlers.append(logging.StreamHandler())
278 312
...@@ -1159,6 +1193,41 @@ class LoginDialog(QDialog): ...@@ -1159,6 +1193,41 @@ class LoginDialog(QDialog):
1159 return getattr(self, 'current_password_hash', '') 1193 return getattr(self, 'current_password_hash', '')
1160 1194
1161 1195
1196 THUMB_REORDER_MIME = "application/x-zb100-thumb-index"
1197
1198
1199 class DraggableThumbnail(QWidget):
1200 """可拖拽重排序的缩略图容器"""
1201
1202 def __init__(self, index: int, parent_window, parent=None):
1203 super().__init__(parent)
1204 self.index = index
1205 self.parent_window = parent_window
1206 self._drag_start_pos = None
1207 self.setCursor(Qt.OpenHandCursor)
1208
1209 def mousePressEvent(self, event: QMouseEvent):
1210 if event.button() == Qt.LeftButton:
1211 self._drag_start_pos = event.pos()
1212 super().mousePressEvent(event)
1213
1214 def mouseMoveEvent(self, event: QMouseEvent):
1215 if self._drag_start_pos is None or not (event.buttons() & Qt.LeftButton):
1216 return
1217 if (event.pos() - self._drag_start_pos).manhattanLength() < QApplication.startDragDistance():
1218 return
1219
1220 drag = QDrag(self)
1221 mime = QMimeData()
1222 mime.setData(THUMB_REORDER_MIME, str(self.index).encode("utf-8"))
1223 drag.setMimeData(mime)
1224 pixmap = self.grab()
1225 drag.setPixmap(pixmap)
1226 drag.setHotSpot(event.pos())
1227 drag.exec(Qt.MoveAction)
1228 self._drag_start_pos = None
1229
1230
1162 class DragDropScrollArea(QScrollArea): 1231 class DragDropScrollArea(QScrollArea):
1163 """自定义支持拖拽的图像滚动区域""" 1232 """自定义支持拖拽的图像滚动区域"""
1164 1233
...@@ -1182,6 +1251,11 @@ class DragDropScrollArea(QScrollArea): ...@@ -1182,6 +1251,11 @@ class DragDropScrollArea(QScrollArea):
1182 """拖拽进入事件处理""" 1251 """拖拽进入事件处理"""
1183 mime_data = event.mimeData() 1252 mime_data = event.mimeData()
1184 1253
1254 # 内部缩略图重排拖拽
1255 if mime_data.hasFormat(THUMB_REORDER_MIME):
1256 event.acceptProposedAction()
1257 return
1258
1185 # 检查是否包含文件URL 1259 # 检查是否包含文件URL
1186 if mime_data.hasUrls(): 1260 if mime_data.hasUrls():
1187 urls = mime_data.urls() 1261 urls = mime_data.urls()
...@@ -1247,6 +1321,19 @@ class DragDropScrollArea(QScrollArea): ...@@ -1247,6 +1321,19 @@ class DragDropScrollArea(QScrollArea):
1247 } 1321 }
1248 """) 1322 """)
1249 1323
1324 # 内部缩略图重排
1325 if mime_data.hasFormat(THUMB_REORDER_MIME):
1326 try:
1327 src_index = int(bytes(mime_data.data(THUMB_REORDER_MIME)).decode("utf-8"))
1328 except (ValueError, UnicodeDecodeError):
1329 event.ignore()
1330 return
1331 target_index = self._compute_reorder_target(event.position().toPoint() if hasattr(event, "position") else event.pos())
1332 if target_index is not None and hasattr(self.parent_window, "reorder_image"):
1333 self.parent_window.reorder_image(src_index, target_index)
1334 event.acceptProposedAction()
1335 return
1336
1250 # 处理文件拖拽 1337 # 处理文件拖拽
1251 if mime_data.hasUrls(): 1338 if mime_data.hasUrls():
1252 urls = mime_data.urls() 1339 urls = mime_data.urls()
...@@ -1263,14 +1350,27 @@ class DragDropScrollArea(QScrollArea): ...@@ -1263,14 +1350,27 @@ class DragDropScrollArea(QScrollArea):
1263 event.acceptProposedAction() 1350 event.acceptProposedAction()
1264 return 1351 return
1265 1352
1266 # 处理剪贴板图像拖拽 1353 # 处理拖入的图像数据(非文件)
1354 # macOS 上 mime_data.imageData() 会触发 NSPasteboard → NSImage 转换,在 macOS 26 上
1355 # 是已知 native crash 路径;改为优先读取 image/* MIME 的原始字节。
1267 try: 1356 try:
1268 if mime_data.hasImage(): 1357 if platform.system() == "Darwin":
1269 image = mime_data.imageData() 1358 for mime_type in ("image/png", "image/jpeg", "image/bmp", "image/tiff"):
1270 if isinstance(image, QImage) and not image.isNull(): 1359 if mime_data.hasFormat(mime_type):
1271 self.parent_window.add_clipboard_image(image.copy()) 1360 raw = mime_data.data(mime_type)
1272 event.acceptProposedAction() 1361 if raw and len(raw) > 0:
1273 return 1362 image = QImage()
1363 if image.loadFromData(bytes(raw)):
1364 self.parent_window.add_clipboard_image(image.copy())
1365 event.acceptProposedAction()
1366 return
1367 else:
1368 if mime_data.hasImage():
1369 image = mime_data.imageData()
1370 if isinstance(image, QImage) and not image.isNull():
1371 self.parent_window.add_clipboard_image(image.copy())
1372 event.acceptProposedAction()
1373 return
1274 except Exception: 1374 except Exception:
1275 pass # 拖放图像数据获取失败,静默忽略 1375 pass # 拖放图像数据获取失败,静默忽略
1276 1376
...@@ -1281,6 +1381,32 @@ class DragDropScrollArea(QScrollArea): ...@@ -1281,6 +1381,32 @@ class DragDropScrollArea(QScrollArea):
1281 valid_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'} 1381 valid_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
1282 return Path(file_path).suffix.lower() in valid_extensions 1382 return Path(file_path).suffix.lower() in valid_extensions
1283 1383
1384 def _compute_reorder_target(self, drop_pos):
1385 """根据 drop 坐标算出目标插入位置(索引)。
1386
1387 策略:落点 x 与每个缩略图的中心 x 比较 —— 小于中心则插在该缩略图之前,大于则之后。
1388 落在所有缩略图右侧则追加到末尾。
1389 """
1390 window = self.parent_window
1391 if not hasattr(window, "img_layout") or not hasattr(window, "uploaded_images"):
1392 return None
1393 layout = window.img_layout
1394 # 映射坐标到 img_container
1395 viewport_pos = self.viewport().mapFrom(self, drop_pos) if self.viewport() else drop_pos
1396 container_pos = window.img_container.mapFrom(self.viewport(), viewport_pos)
1397 drop_x = container_pos.x()
1398
1399 thumb_count = len(window.uploaded_images)
1400 for i in range(thumb_count):
1401 item = layout.itemAt(i)
1402 if item is None or item.widget() is None:
1403 continue
1404 w = item.widget()
1405 center_x = w.x() + w.width() / 2
1406 if drop_x < center_x:
1407 return i
1408 return thumb_count
1409
1284 1410
1285 class ImageGeneratorWindow(QMainWindow): 1411 class ImageGeneratorWindow(QMainWindow):
1286 """Qt-based main application window""" 1412 """Qt-based main application window"""
...@@ -2078,20 +2204,27 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2078,20 +2204,27 @@ class ImageGeneratorWindow(QMainWindow):
2078 2204
2079 注意:Finder 复制的文件应通过 paste_from_clipboard 中的 hasUrls 分支处理, 2205 注意:Finder 复制的文件应通过 paste_from_clipboard 中的 hasUrls 分支处理,
2080 不会走到这里。此方法只处理纯图像数据(截图、从应用复制的图片等)。 2206 不会走到这里。此方法只处理纯图像数据(截图、从应用复制的图片等)。
2207
2208 macOS 26 (Darwin 25+) 上 Qt 的 mimeData.imageData() / clipboard.image() /
2209 application/x-qt-image 反序列化会触发 NSPasteboard → NSImage 转换,
2210 在部分场景下 native crash,因此这些路径在 macOS 上一律禁用,改走 osascript。
2081 """ 2211 """
2082 self.logger.info("[clipboard] 开始获取剪贴板图像...") 2212 self.logger.info("[clipboard] 开始获取剪贴板图像...")
2083 clipboard = QApplication.clipboard() 2213 clipboard = QApplication.clipboard()
2214 is_mac = platform.system() == "Darwin"
2084 2215
2085 # 方法1: 从 MIME data 读取图像 2216 # 方法1: 从 MIME data 读取原始字节(安全 —— 只读 bytes,不触发 NSImage 转换)
2086 # 避免直接调用 clipboard.image(),该方法在 macOS 26 可能导致 native crash
2087 try: 2217 try:
2088 mime_data = clipboard.mimeData() 2218 mime_data = clipboard.mimeData()
2089 if mime_data is not None: 2219 if mime_data is not None:
2090 available_formats = mime_data.formats() 2220 available_formats = list(mime_data.formats())
2091 self.logger.info(f"[clipboard] 可用 MIME 格式: {list(available_formats)}") 2221 self.logger.info(f"[clipboard] 可用 MIME 格式: {available_formats}")
2092 # 尝试从图像 MIME 格式读取原始字节 2222 # macOS 上跳过 application/x-qt-image(Qt 内部格式,反序列化可能崩溃)
2093 for mime_type in ("image/png", "image/jpeg", "image/bmp", "image/tiff", "application/x-qt-image"): 2223 mime_candidates = ["image/png", "image/jpeg", "image/bmp", "image/tiff"]
2094 if mime_type in mime_data.formats(): 2224 if not is_mac:
2225 mime_candidates.append("application/x-qt-image")
2226 for mime_type in mime_candidates:
2227 if mime_type in available_formats:
2095 raw_data = mime_data.data(mime_type) 2228 raw_data = mime_data.data(mime_type)
2096 self.logger.info(f"[clipboard] 从 {mime_type} 读取到 {len(raw_data)} bytes") 2229 self.logger.info(f"[clipboard] 从 {mime_type} 读取到 {len(raw_data)} bytes")
2097 if raw_data and len(raw_data) > 0: 2230 if raw_data and len(raw_data) > 0:
...@@ -2101,8 +2234,8 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2101,8 +2234,8 @@ class ImageGeneratorWindow(QMainWindow):
2101 f"size={image.width()}x{image.height()}") 2234 f"size={image.width()}x{image.height()}")
2102 return image.copy() 2235 return image.copy()
2103 2236
2104 # 尝试 hasImage + imageData 2237 # 非 macOS 上尝试 hasImage + imageData(macOS 禁用,会 native crash)
2105 if mime_data.hasImage(): 2238 if not is_mac and mime_data.hasImage():
2106 self.logger.info("[clipboard] 尝试 hasImage + imageData 方式") 2239 self.logger.info("[clipboard] 尝试 hasImage + imageData 方式")
2107 image_data = mime_data.imageData() 2240 image_data = mime_data.imageData()
2108 if isinstance(image_data, QImage) and not image_data.isNull(): 2241 if isinstance(image_data, QImage) and not image_data.isNull():
...@@ -2114,12 +2247,12 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2114,12 +2247,12 @@ class ImageGeneratorWindow(QMainWindow):
2114 except Exception as e: 2247 except Exception as e:
2115 self.logger.warning(f"MIME data 方式获取剪贴板图像失败: {e}") 2248 self.logger.warning(f"MIME data 方式获取剪贴板图像失败: {e}")
2116 2249
2117 # 方法2: macOS 专用 - 用 osascript 将剪贴板图片写入临时文件 2250 # 方法2: macOS 专用 - 用 osascript 将剪贴板图片写入临时文件(最可靠路径)
2118 if platform.system() == "Darwin": 2251 if is_mac:
2119 try: 2252 try:
2120 import subprocess 2253 import subprocess
2121 temp_path = Path(tempfile.gettempdir()) / "nano_banana_app" / "_clipboard_tmp.png" 2254 temp_path = Path(tempfile.gettempdir()) / "nano_banana_app" / "_clipboard_tmp.png"
2122 temp_path.parent.mkdir(exist_ok=True) 2255 temp_path.parent.mkdir(parents=True, exist_ok=True)
2123 if temp_path.exists(): 2256 if temp_path.exists():
2124 temp_path.unlink() 2257 temp_path.unlink()
2125 2258
...@@ -2141,17 +2274,19 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2141,17 +2274,19 @@ class ImageGeneratorWindow(QMainWindow):
2141 if result.returncode == 0 and temp_path.exists() and temp_path.stat().st_size > 0: 2274 if result.returncode == 0 and temp_path.exists() and temp_path.stat().st_size > 0:
2142 image = QImage(str(temp_path)) 2275 image = QImage(str(temp_path))
2143 if not image.isNull(): 2276 if not image.isNull():
2277 self.logger.info("[clipboard] osascript 方式成功")
2144 return image.copy() 2278 return image.copy()
2145 except Exception as e: 2279 except Exception as e:
2146 self.logger.warning(f"osascript 方式获取剪贴板图像失败: {e}") 2280 self.logger.warning(f"osascript 方式获取剪贴板图像失败: {e}")
2147 2281
2148 # 方法3: 直接调用 clipboard.image()(低版本 macOS / Windows / Linux) 2282 # 方法3: clipboard.image() 仅在非 macOS 上使用(macOS 26 上会 native crash)
2149 try: 2283 if not is_mac:
2150 image = clipboard.image() 2284 try:
2151 if image and not image.isNull(): 2285 image = clipboard.image()
2152 return image.copy() 2286 if image and not image.isNull():
2153 except Exception as e: 2287 return image.copy()
2154 self.logger.warning(f"clipboard.image() 失败: {e}") 2288 except Exception as e:
2289 self.logger.warning(f"clipboard.image() 失败: {e}")
2155 2290
2156 return None 2291 return None
2157 2292
...@@ -2289,8 +2424,8 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2289,8 +2424,8 @@ class ImageGeneratorWindow(QMainWindow):
2289 2424
2290 pixmap = pixmap.scaled(100, 100, Qt.KeepAspectRatio, Qt.SmoothTransformation) 2425 pixmap = pixmap.scaled(100, 100, Qt.KeepAspectRatio, Qt.SmoothTransformation)
2291 2426
2292 # Container 2427 # Container (draggable for reorder)
2293 container = QWidget() 2428 container = DraggableThumbnail(idx, self)
2294 container_layout = QVBoxLayout() 2429 container_layout = QVBoxLayout()
2295 container_layout.setContentsMargins(5, 5, 5, 5) 2430 container_layout.setContentsMargins(5, 5, 5, 5)
2296 2431
...@@ -2350,6 +2485,30 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2350,6 +2485,30 @@ class ImageGeneratorWindow(QMainWindow):
2350 else: 2485 else:
2351 self.logger.warning(f"无效的图片索引: {index}") 2486 self.logger.warning(f"无效的图片索引: {index}")
2352 2487
2488 def reorder_image(self, src_index: int, target_index: int):
2489 """在 uploaded_images 里把 src 拖到 target 位置。
2490
2491 target_index 语义:插入前,所以从 src 向右移动时需要 -1。
2492 """
2493 n = len(self.uploaded_images)
2494 if not (0 <= src_index < n):
2495 return
2496 if target_index < 0:
2497 target_index = 0
2498 if target_index > n:
2499 target_index = n
2500 # 向右移动时,移除后目标索引 -1
2501 if target_index > src_index:
2502 target_index -= 1
2503 if target_index == src_index:
2504 return
2505 item = self.uploaded_images.pop(src_index)
2506 self.uploaded_images.insert(target_index, item)
2507 self.logger.info(f"缩略图重排: {src_index} -> {target_index}")
2508 self.update_image_preview()
2509 self.status_label.setText(f"● 已调整图片顺序 (图 {src_index + 1} → 图 {target_index + 1})")
2510 self.status_label.setStyleSheet("QLabel { color: #007AFF; }")
2511
2353 def update_saved_prompts_list(self): 2512 def update_saved_prompts_list(self):
2354 """Update the saved prompts dropdown""" 2513 """Update the saved prompts dropdown"""
2355 # Block signals to avoid triggering load_saved_prompt during update 2514 # Block signals to avoid triggering load_saved_prompt during update
...@@ -3102,7 +3261,7 @@ class ImageGenerationWorker(QThread): ...@@ -3102,7 +3261,7 @@ class ImageGenerationWorker(QThread):
3102 if "gemini-3-pro-image-preview" in self.model: 3261 if "gemini-3-pro-image-preview" in self.model:
3103 # Gemini 3 Pro supports both aspect_ratio and image_size 3262 # Gemini 3 Pro supports both aspect_ratio and image_size
3104 config = types.GenerateContentConfig( 3263 config = types.GenerateContentConfig(
3105 response_modalities=["IMAGE"], 3264 response_modalities=["TEXT", "IMAGE"],
3106 image_config=types.ImageConfig( 3265 image_config=types.ImageConfig(
3107 aspect_ratio=self.aspect_ratio, 3266 aspect_ratio=self.aspect_ratio,
3108 image_size=self.image_size 3267 image_size=self.image_size
...@@ -3111,7 +3270,7 @@ class ImageGenerationWorker(QThread): ...@@ -3111,7 +3270,7 @@ class ImageGenerationWorker(QThread):
3111 else: 3270 else:
3112 # Gemini 2.5 Flash only supports aspect_ratio (fixed 1024px output) 3271 # Gemini 2.5 Flash only supports aspect_ratio (fixed 1024px output)
3113 config = types.GenerateContentConfig( 3272 config = types.GenerateContentConfig(
3114 response_modalities=["IMAGE"], 3273 response_modalities=["TEXT", "IMAGE"],
3115 image_config=types.ImageConfig( 3274 image_config=types.ImageConfig(
3116 aspect_ratio=self.aspect_ratio 3275 aspect_ratio=self.aspect_ratio
3117 ) 3276 )
...@@ -3125,7 +3284,9 @@ class ImageGenerationWorker(QThread): ...@@ -3125,7 +3284,9 @@ class ImageGenerationWorker(QThread):
3125 ) 3284 )
3126 3285
3127 # Extract image 3286 # Extract image
3128 for part in response.parts: 3287 text_fragments = []
3288 parts = response.parts or []
3289 for part in parts:
3129 if hasattr(part, 'inline_data') and part.inline_data: 3290 if hasattr(part, 'inline_data') and part.inline_data:
3130 if isinstance(part.inline_data.data, bytes): 3291 if isinstance(part.inline_data.data, bytes):
3131 image_bytes = part.inline_data.data 3292 image_bytes = part.inline_data.data
...@@ -3145,8 +3306,18 @@ class ImageGenerationWorker(QThread): ...@@ -3145,8 +3306,18 @@ class ImageGenerationWorker(QThread):
3145 self.finished.emit(image_bytes, self.prompt, reference_images_bytes, 3306 self.finished.emit(image_bytes, self.prompt, reference_images_bytes,
3146 self.aspect_ratio, self.image_size, self.model) 3307 self.aspect_ratio, self.image_size, self.model)
3147 return 3308 return
3309 if getattr(part, 'text', None):
3310 text_fragments.append(part.text)
3148 3311
3149 error_msg = "响应中没有图片数据" 3312 finish_reason = ""
3313 try:
3314 finish_reason = str(response.candidates[0].finish_reason)
3315 except Exception:
3316 pass
3317 detail = " | ".join(t for t in text_fragments if t).strip()
3318 error_msg = f"响应中没有图片数据 (finish_reason={finish_reason})"
3319 if detail:
3320 error_msg += f"\n模型说明: {detail}"
3150 self.logger.error(error_msg) 3321 self.logger.error(error_msg)
3151 self.error.emit(error_msg) 3322 self.error.emit(error_msg)
3152 3323
...@@ -4203,6 +4374,14 @@ def main(): ...@@ -4203,6 +4374,14 @@ def main():
4203 logger.info("[BOOT] Phase 2: 记录系统环境...") 4374 logger.info("[BOOT] Phase 2: 记录系统环境...")
4204 _log_system_info() 4375 _log_system_info()
4205 4376
4377 # 第 2.5 步:清理遗留的剪贴板临时文件(长时间运行防护)
4378 try:
4379 removed = _cleanup_clipboard_tempfiles(max_age_hours=24)
4380 if removed > 0:
4381 logger.info(f"[BOOT] 已清理 {removed} 个超过 24 小时的剪贴板临时文件")
4382 except Exception as e:
4383 logger.warning(f"[BOOT] 清理剪贴板临时文件失败: {e}")
4384
4206 # 第3步:加载配置 4385 # 第3步:加载配置
4207 logger.info("[BOOT] Phase 3: 加载配置文件...") 4386 logger.info("[BOOT] Phase 3: 加载配置文件...")
4208 4387
......