增加使用人的信息记录与日志留存
Showing
2 changed files
with
172 additions
and
12 deletions
| ... | @@ -1091,6 +1091,7 @@ class ImageGeneratorWindow(QMainWindow): | ... | @@ -1091,6 +1091,7 @@ class ImageGeneratorWindow(QMainWindow): |
| 1091 | self.db_config = None | 1091 | self.db_config = None |
| 1092 | self.last_user = "" | 1092 | self.last_user = "" |
| 1093 | self.saved_password_hash = "" | 1093 | self.saved_password_hash = "" |
| 1094 | self.authenticated_user = "" # 当前登录用户名 | ||
| 1094 | 1095 | ||
| 1095 | self.load_config() | 1096 | self.load_config() |
| 1096 | self.set_window_icon() | 1097 | self.set_window_icon() |
| ... | @@ -1401,21 +1402,21 @@ class ImageGeneratorWindow(QMainWindow): | ... | @@ -1401,21 +1402,21 @@ class ImageGeneratorWindow(QMainWindow): |
| 1401 | 1402 | ||
| 1402 | self.image_count_label = QLabel("已选择 0 张") | 1403 | self.image_count_label = QLabel("已选择 0 张") |
| 1403 | upload_header.addWidget(self.image_count_label) | 1404 | upload_header.addWidget(self.image_count_label) |
| 1405 | |||
| 1406 | # Drag and drop hint - 紧凑显示在右侧 | ||
| 1407 | hint_label = QLabel("💡 拖拽或粘贴图片到下方区域") | ||
| 1408 | hint_label.setStyleSheet("QLabel { color: #666666; font-size: 11px; margin-left: 10px; }") | ||
| 1409 | upload_header.addWidget(hint_label) | ||
| 1410 | |||
| 1404 | upload_header.addStretch() | 1411 | upload_header.addStretch() |
| 1405 | ref_layout.addLayout(upload_header) | 1412 | ref_layout.addLayout(upload_header) |
| 1406 | 1413 | ||
| 1407 | # Drag and drop hint | ||
| 1408 | hint_label = QLabel("💡 提示:可以直接拖拽图片到下方区域,或使用 Ctrl+V 粘贴截图") | ||
| 1409 | hint_label.setStyleSheet("QLabel { color: #666666; font-size: 11px; margin: 2px 0; }") | ||
| 1410 | hint_label.setWordWrap(True) | ||
| 1411 | ref_layout.addWidget(hint_label) | ||
| 1412 | |||
| 1413 | # Image preview scroll area with drag-and-drop support | 1414 | # Image preview scroll area with drag-and-drop support |
| 1414 | self.img_scroll = DragDropScrollArea(self) | 1415 | self.img_scroll = DragDropScrollArea(self) |
| 1415 | self.img_scroll.setWidgetResizable(True) | 1416 | self.img_scroll.setWidgetResizable(True) |
| 1416 | self.img_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) | 1417 | self.img_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) |
| 1417 | self.img_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) | 1418 | self.img_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) |
| 1418 | self.img_scroll.setFixedHeight(180) | 1419 | self.img_scroll.setFixedHeight(220) |
| 1419 | 1420 | ||
| 1420 | self.img_container = QWidget() | 1421 | self.img_container = QWidget() |
| 1421 | self.img_layout = QHBoxLayout() | 1422 | self.img_layout = QHBoxLayout() |
| ... | @@ -2098,6 +2099,7 @@ class ImageGeneratorWindow(QMainWindow): | ... | @@ -2098,6 +2099,7 @@ class ImageGeneratorWindow(QMainWindow): |
| 2098 | 2099 | ||
| 2099 | try: | 2100 | try: |
| 2100 | # Submit task to queue | 2101 | # Submit task to queue |
| 2102 | import socket | ||
| 2101 | task_id = self.task_manager.submit_task( | 2103 | task_id = self.task_manager.submit_task( |
| 2102 | task_type=TaskType.IMAGE_GENERATION, | 2104 | task_type=TaskType.IMAGE_GENERATION, |
| 2103 | prompt=prompt, | 2105 | prompt=prompt, |
| ... | @@ -2105,7 +2107,9 @@ class ImageGeneratorWindow(QMainWindow): | ... | @@ -2105,7 +2107,9 @@ class ImageGeneratorWindow(QMainWindow): |
| 2105 | reference_images=self.uploaded_images.copy(), | 2107 | reference_images=self.uploaded_images.copy(), |
| 2106 | aspect_ratio=self.aspect_ratio.currentText(), | 2108 | aspect_ratio=self.aspect_ratio.currentText(), |
| 2107 | image_size=self.image_size.currentText(), | 2109 | image_size=self.image_size.currentText(), |
| 2108 | model="gemini-3-pro-image-preview" | 2110 | model="gemini-3-pro-image-preview", |
| 2111 | user_name=self.authenticated_user, | ||
| 2112 | device_name=socket.gethostname() | ||
| 2109 | ) | 2113 | ) |
| 2110 | 2114 | ||
| 2111 | # Connect to task completion signal | 2115 | # Connect to task completion signal |
| ... | @@ -3520,6 +3524,7 @@ class StyleDesignerTab(QWidget): | ... | @@ -3520,6 +3524,7 @@ class StyleDesignerTab(QWidget): |
| 3520 | 3524 | ||
| 3521 | try: | 3525 | try: |
| 3522 | # Submit task to queue | 3526 | # Submit task to queue |
| 3527 | import socket | ||
| 3523 | task_id = self.parent_window.task_manager.submit_task( | 3528 | task_id = self.parent_window.task_manager.submit_task( |
| 3524 | task_type=TaskType.STYLE_DESIGN, | 3529 | task_type=TaskType.STYLE_DESIGN, |
| 3525 | prompt=prompt, | 3530 | prompt=prompt, |
| ... | @@ -3527,7 +3532,9 @@ class StyleDesignerTab(QWidget): | ... | @@ -3527,7 +3532,9 @@ class StyleDesignerTab(QWidget): |
| 3527 | reference_images=[], | 3532 | reference_images=[], |
| 3528 | aspect_ratio=aspect_ratio, | 3533 | aspect_ratio=aspect_ratio, |
| 3529 | image_size=image_size, | 3534 | image_size=image_size, |
| 3530 | model=model | 3535 | model=model, |
| 3536 | user_name=self.parent_window.authenticated_user, | ||
| 3537 | device_name=socket.gethostname() | ||
| 3531 | ) | 3538 | ) |
| 3532 | 3539 | ||
| 3533 | # Connect to task completion signal | 3540 | # Connect to task completion signal |
| ... | @@ -3861,6 +3868,7 @@ def main(): | ... | @@ -3861,6 +3868,7 @@ def main(): |
| 3861 | 3868 | ||
| 3862 | # Show main window | 3869 | # Show main window |
| 3863 | main_window = ImageGeneratorWindow() | 3870 | main_window = ImageGeneratorWindow() |
| 3871 | main_window.authenticated_user = authenticated_user # 设置登录用户名 | ||
| 3864 | main_window.show() | 3872 | main_window.show() |
| 3865 | 3873 | ||
| 3866 | sys.exit(app.exec()) | 3874 | sys.exit(app.exec()) | ... | ... |
| ... | @@ -53,8 +53,12 @@ class Task: | ... | @@ -53,8 +53,12 @@ class Task: |
| 53 | image_size: str | 53 | image_size: str |
| 54 | model: str | 54 | model: str |
| 55 | 55 | ||
| 56 | # 用户信息 (用于日志记录) | ||
| 57 | user_name: str = "" | ||
| 58 | device_name: str = "" | ||
| 59 | |||
| 56 | # 时间戳 | 60 | # 时间戳 |
| 57 | created_at: datetime | 61 | created_at: datetime = field(default_factory=datetime.now) |
| 58 | started_at: Optional[datetime] = None | 62 | started_at: Optional[datetime] = None |
| 59 | completed_at: Optional[datetime] = None | 63 | completed_at: Optional[datetime] = None |
| 60 | 64 | ||
| ... | @@ -102,9 +106,28 @@ class TaskQueueManager(QObject): | ... | @@ -102,9 +106,28 @@ class TaskQueueManager(QObject): |
| 102 | self._max_queue_size = 10 | 106 | self._max_queue_size = 10 |
| 103 | self._max_history_size = 10 # 只保留最近10条完成任务 | 107 | self._max_history_size = 10 # 只保留最近10条完成任务 |
| 104 | 108 | ||
| 109 | # 加载数据库配置用于日志记录 | ||
| 110 | self._db_config = None | ||
| 111 | self._load_db_config() | ||
| 112 | |||
| 105 | self._initialized = True | 113 | self._initialized = True |
| 106 | self.logger.info("TaskQueueManager 初始化完成") | 114 | self.logger.info("TaskQueueManager 初始化完成") |
| 107 | 115 | ||
| 116 | def _load_db_config(self): | ||
| 117 | """加载数据库配置""" | ||
| 118 | try: | ||
| 119 | import json | ||
| 120 | from pathlib import Path | ||
| 121 | config_file = Path("config.json") | ||
| 122 | if config_file.exists(): | ||
| 123 | with open(config_file, 'r', encoding='utf-8') as f: | ||
| 124 | config = json.load(f) | ||
| 125 | self._db_config = config.get('db_config') | ||
| 126 | if self._db_config: | ||
| 127 | self.logger.info("数据库配置已加载") | ||
| 128 | except Exception as e: | ||
| 129 | self.logger.warning(f"加载数据库配置失败: {e}") | ||
| 130 | |||
| 108 | def submit_task( | 131 | def submit_task( |
| 109 | self, | 132 | self, |
| 110 | task_type: TaskType, | 133 | task_type: TaskType, |
| ... | @@ -113,7 +136,9 @@ class TaskQueueManager(QObject): | ... | @@ -113,7 +136,9 @@ class TaskQueueManager(QObject): |
| 113 | reference_images: List[str], | 136 | reference_images: List[str], |
| 114 | aspect_ratio: str, | 137 | aspect_ratio: str, |
| 115 | image_size: str, | 138 | image_size: str, |
| 116 | model: str | 139 | model: str, |
| 140 | user_name: str = "", | ||
| 141 | device_name: str = "" | ||
| 117 | ) -> str: | 142 | ) -> str: |
| 118 | """ | 143 | """ |
| 119 | 提交新任务到队列 | 144 | 提交新任务到队列 |
| ... | @@ -126,6 +151,8 @@ class TaskQueueManager(QObject): | ... | @@ -126,6 +151,8 @@ class TaskQueueManager(QObject): |
| 126 | aspect_ratio: 宽高比 | 151 | aspect_ratio: 宽高比 |
| 127 | image_size: 图片尺寸 | 152 | image_size: 图片尺寸 |
| 128 | model: 模型名称 | 153 | model: 模型名称 |
| 154 | user_name: 用户名 (用于日志记录) | ||
| 155 | device_name: 设备名称 (用于日志记录) | ||
| 129 | 156 | ||
| 130 | Returns: | 157 | Returns: |
| 131 | task_id: 任务唯一标识 | 158 | task_id: 任务唯一标识 |
| ... | @@ -148,6 +175,8 @@ class TaskQueueManager(QObject): | ... | @@ -148,6 +175,8 @@ class TaskQueueManager(QObject): |
| 148 | aspect_ratio=aspect_ratio, | 175 | aspect_ratio=aspect_ratio, |
| 149 | image_size=image_size, | 176 | image_size=image_size, |
| 150 | model=model, | 177 | model=model, |
| 178 | user_name=user_name, | ||
| 179 | device_name=device_name, | ||
| 151 | created_at=datetime.now() | 180 | created_at=datetime.now() |
| 152 | ) | 181 | ) |
| 153 | 182 | ||
| ... | @@ -225,6 +254,9 @@ class TaskQueueManager(QObject): | ... | @@ -225,6 +254,9 @@ class TaskQueueManager(QObject): |
| 225 | elapsed = (task.completed_at - task.started_at).total_seconds() | 254 | elapsed = (task.completed_at - task.started_at).total_seconds() |
| 226 | self.logger.info(f"任务完成: {task_id[:8]} - 耗时 {elapsed:.1f}s") | 255 | self.logger.info(f"任务完成: {task_id[:8]} - 耗时 {elapsed:.1f}s") |
| 227 | 256 | ||
| 257 | # 记录使用日志 | ||
| 258 | self._log_usage(task_id, 'success', 'memory', None) | ||
| 259 | |||
| 228 | self.task_completed.emit(task_id, image_bytes, prompt, reference_images, | 260 | self.task_completed.emit(task_id, image_bytes, prompt, reference_images, |
| 229 | aspect_ratio, image_size, model) | 261 | aspect_ratio, image_size, model) |
| 230 | 262 | ||
| ... | @@ -247,6 +279,9 @@ class TaskQueueManager(QObject): | ... | @@ -247,6 +279,9 @@ class TaskQueueManager(QObject): |
| 247 | 279 | ||
| 248 | self.logger.error(f"任务失败: {task_id[:8]} - {error}") | 280 | self.logger.error(f"任务失败: {task_id[:8]} - {error}") |
| 249 | 281 | ||
| 282 | # 记录使用日志 | ||
| 283 | self._log_usage(task_id, 'failure', None, error) | ||
| 284 | |||
| 250 | self.task_failed.emit(task_id, error) | 285 | self.task_failed.emit(task_id, error) |
| 251 | 286 | ||
| 252 | # 清理旧任务历史 | 287 | # 清理旧任务历史 |
| ... | @@ -305,11 +340,86 @@ class TaskQueueManager(QObject): | ... | @@ -305,11 +340,86 @@ class TaskQueueManager(QObject): |
| 305 | return sum(1 for t in self._tasks.values() if t.status == TaskStatus.RUNNING) | 340 | return sum(1 for t in self._tasks.values() if t.status == TaskStatus.RUNNING) |
| 306 | 341 | ||
| 307 | def cancel_task(self, task_id: str): | 342 | def cancel_task(self, task_id: str): |
| 308 | """取消任务 (仅等待中任务)""" | 343 | """ |
| 344 | 取消任务 (仅等待中任务) | ||
| 345 | 将任务状态设为 CANCELLED 并从队列中移除 | ||
| 346 | """ | ||
| 309 | task = self._tasks.get(task_id) | 347 | task = self._tasks.get(task_id) |
| 310 | if task and task.status == TaskStatus.PENDING: | 348 | if task and task.status == TaskStatus.PENDING: |
| 311 | task.status = TaskStatus.CANCELLED | 349 | task.status = TaskStatus.CANCELLED |
| 350 | task.completed_at = datetime.now() | ||
| 351 | |||
| 352 | # 从队列中移除任务 (需要重建队列以移除特定ID) | ||
| 353 | temp_queue = Queue() | ||
| 354 | while not self._queue.empty(): | ||
| 355 | tid = self._queue.get() | ||
| 356 | if tid != task_id: | ||
| 357 | temp_queue.put(tid) | ||
| 358 | |||
| 359 | # 替换原队列 | ||
| 360 | self._queue = temp_queue | ||
| 361 | |||
| 312 | self.logger.info(f"任务已取消: {task_id[:8]}") | 362 | self.logger.info(f"任务已取消: {task_id[:8]}") |
| 363 | self.task_failed.emit(task_id, "用户取消") # 发送取消信号以更新 UI | ||
| 364 | |||
| 365 | def _log_usage(self, task_id: str, status: str, result_path: Optional[str], error_message: Optional[str]): | ||
| 366 | """ | ||
| 367 | 记录用户使用日志到数据库 | ||
| 368 | |||
| 369 | Args: | ||
| 370 | task_id: 任务ID | ||
| 371 | status: 'success' 或 'failure' | ||
| 372 | result_path: 成功时的图片路径,失败时为 None | ||
| 373 | error_message: 失败时的错误信息,成功时为 None | ||
| 374 | """ | ||
| 375 | if not self._db_config: | ||
| 376 | self.logger.debug("数据库配置未加载,跳过日志记录") | ||
| 377 | return | ||
| 378 | |||
| 379 | task = self._tasks.get(task_id) | ||
| 380 | if not task: | ||
| 381 | self.logger.warning(f"任务 {task_id[:8]} 不存在,无法记录日志") | ||
| 382 | return | ||
| 383 | |||
| 384 | try: | ||
| 385 | import pymysql | ||
| 386 | |||
| 387 | # 处理未登录用户 | ||
| 388 | user_name = task.user_name if task.user_name else "未知用户" | ||
| 389 | device_name = task.device_name if task.device_name else "未知设备" | ||
| 390 | |||
| 391 | # 连接数据库 | ||
| 392 | connection = pymysql.connect( | ||
| 393 | host=self._db_config['host'], | ||
| 394 | port=self._db_config['port'], | ||
| 395 | user=self._db_config['user'], | ||
| 396 | password=self._db_config['password'], | ||
| 397 | database=self._db_config['database'] | ||
| 398 | ) | ||
| 399 | |||
| 400 | with connection.cursor() as cursor: | ||
| 401 | sql = """ | ||
| 402 | INSERT INTO `nano_banana_user_use_log` | ||
| 403 | (`user_name`, `device_name`, `prompt`, `result_path`, `status`, `error_message`) | ||
| 404 | VALUES (%s, %s, %s, %s, %s, %s) | ||
| 405 | """ | ||
| 406 | cursor.execute(sql, ( | ||
| 407 | user_name, | ||
| 408 | device_name, | ||
| 409 | task.prompt, | ||
| 410 | result_path, | ||
| 411 | status, | ||
| 412 | error_message | ||
| 413 | )) | ||
| 414 | |||
| 415 | connection.commit() | ||
| 416 | connection.close() | ||
| 417 | |||
| 418 | self.logger.info(f"使用日志已记录: {task_id[:8]} - {status}") | ||
| 419 | |||
| 420 | except Exception as e: | ||
| 421 | # 日志记录失败不应影响主流程 | ||
| 422 | self.logger.error(f"记录使用日志失败: {e}", exc_info=False) | ||
| 313 | 423 | ||
| 314 | 424 | ||
| 315 | class TaskQueueWidget(QWidget): | 425 | class TaskQueueWidget(QWidget): |
| ... | @@ -338,6 +448,7 @@ class TaskQueueWidget(QWidget): | ... | @@ -338,6 +448,7 @@ class TaskQueueWidget(QWidget): |
| 338 | title = QLabel("任务队列") | 448 | title = QLabel("任务队列") |
| 339 | title.setStyleSheet("QLabel { font-weight: bold; font-size: 10px; color: #666; }") | 449 | title.setStyleSheet("QLabel { font-weight: bold; font-size: 10px; color: #666; }") |
| 340 | title.setAlignment(Qt.AlignCenter) | 450 | title.setAlignment(Qt.AlignCenter) |
| 451 | title.setToolTip("鼠标悬停查看详情\n右键等待中的任务可取消") | ||
| 341 | layout.addWidget(title) | 452 | layout.addWidget(title) |
| 342 | 453 | ||
| 343 | # 分隔线 | 454 | # 分隔线 |
| ... | @@ -367,6 +478,11 @@ class TaskQueueWidget(QWidget): | ... | @@ -367,6 +478,11 @@ class TaskQueueWidget(QWidget): |
| 367 | } | 478 | } |
| 368 | """) | 479 | """) |
| 369 | self.task_list.itemClicked.connect(self._on_task_item_clicked) | 480 | self.task_list.itemClicked.connect(self._on_task_item_clicked) |
| 481 | |||
| 482 | # 启用右键菜单 | ||
| 483 | self.task_list.setContextMenuPolicy(Qt.CustomContextMenu) | ||
| 484 | self.task_list.customContextMenuRequested.connect(self._show_context_menu) | ||
| 485 | |||
| 370 | layout.addWidget(self.task_list) | 486 | layout.addWidget(self.task_list) |
| 371 | 487 | ||
| 372 | layout.addStretch() | 488 | layout.addStretch() |
| ... | @@ -426,6 +542,15 @@ class TaskQueueWidget(QWidget): | ... | @@ -426,6 +542,15 @@ class TaskQueueWidget(QWidget): |
| 426 | from PySide6.QtGui import QBrush, QColor | 542 | from PySide6.QtGui import QBrush, QColor |
| 427 | item.setForeground(QBrush(QColor(color))) | 543 | item.setForeground(QBrush(QColor(color))) |
| 428 | 544 | ||
| 545 | # 设置 Tooltip - 显示完整的 Prompt 和任务信息 | ||
| 546 | tooltip_text = f"Prompt: {task.prompt}\n" | ||
| 547 | tooltip_text += f"创建时间: {task.created_at.strftime('%Y-%m-%d %H:%M:%S')}\n" | ||
| 548 | tooltip_text += f"宽高比: {task.aspect_ratio}\n" | ||
| 549 | tooltip_text += f"尺寸: {task.image_size}" | ||
| 550 | if task.error_message: | ||
| 551 | tooltip_text += f"\n错误: {task.error_message}" | ||
| 552 | item.setToolTip(tooltip_text) | ||
| 553 | |||
| 429 | # 添加到列表 | 554 | # 添加到列表 |
| 430 | self.task_list.addItem(item) | 555 | self.task_list.addItem(item) |
| 431 | 556 | ||
| ... | @@ -434,6 +559,33 @@ class TaskQueueWidget(QWidget): | ... | @@ -434,6 +559,33 @@ class TaskQueueWidget(QWidget): |
| 434 | for i in range(self.task_list.count() - 10): | 559 | for i in range(self.task_list.count() - 10): |
| 435 | self.task_list.takeItem(0) | 560 | self.task_list.takeItem(0) |
| 436 | 561 | ||
| 562 | def _show_context_menu(self, position): | ||
| 563 | """显示右键菜单 - 仅为等待中的任务提供取消选项""" | ||
| 564 | from PySide6.QtWidgets import QMenu | ||
| 565 | |||
| 566 | # 获取点击的任务项 | ||
| 567 | item = self.task_list.itemAt(position) | ||
| 568 | if not item: | ||
| 569 | return | ||
| 570 | |||
| 571 | task_id = item.data(Qt.UserRole) | ||
| 572 | if not task_id: | ||
| 573 | return | ||
| 574 | |||
| 575 | task = self.manager.get_task(task_id) | ||
| 576 | if not task: | ||
| 577 | return | ||
| 578 | |||
| 579 | # 仅为 PENDING 状态的任务显示取消选项 | ||
| 580 | if task.status == TaskStatus.PENDING: | ||
| 581 | menu = QMenu() | ||
| 582 | cancel_action = menu.addAction("取消任务") | ||
| 583 | |||
| 584 | action = menu.exec_(self.task_list.mapToGlobal(position)) | ||
| 585 | |||
| 586 | if action == cancel_action: | ||
| 587 | self.manager.cancel_task(task_id) | ||
| 588 | self._update_summary() | ||
| 437 | 589 | ||
| 438 | def _on_task_added(self, task: Task): | 590 | def _on_task_added(self, task: Task): |
| 439 | """任务添加回调""" | 591 | """任务添加回调""" | ... | ... |
-
Please register or sign in to post a comment