task_queue.py 28.3 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817
"""
任务队列系统
提供异步图像生成任务的队列管理和 UI 组件
"""

from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Optional, List, Dict
from queue import Queue
from threading import Lock
import uuid
import logging
import io

from PySide6.QtCore import QObject, Signal, QTimer, Qt
from PySide6.QtWidgets import (
    QWidget, QVBoxLayout, QHBoxLayout, QLabel,
    QPushButton, QListWidget, QListWidgetItem, QDialog, QScrollArea, QFrame
)
from PySide6.QtGui import QPixmap, QMouseEvent
from PIL import Image


class TaskType(Enum):
    """任务类型"""
    IMAGE_GENERATION = "image_gen"
    STYLE_DESIGN = "style_design"


class TaskStatus(Enum):
    """任务状态"""
    PENDING = "pending"
    RUNNING = "running"
    COMPLETED = "completed"
    FAILED = "failed"
    CANCELLED = "cancelled"


@dataclass
class Task:
    """任务数据模型"""
    # 标识
    id: str
    type: TaskType
    status: TaskStatus

    # 输入参数
    prompt: str
    api_key: str
    reference_images: List[str]
    aspect_ratio: str
    image_size: str
    model: str

    # 用户信息 (用于日志记录)
    user_name: str = ""
    device_name: str = ""

    # 时间戳
    created_at: datetime = field(default_factory=datetime.now)
    started_at: Optional[datetime] = None
    completed_at: Optional[datetime] = None

    # 结果
    result_bytes: Optional[bytes] = None
    error_message: Optional[str] = None

    # UI 相关
    thumbnail: Optional[bytes] = None
    progress: float = 0.0


class TaskQueueManager(QObject):
    """
    单例任务队列管理器
    管理所有图像生成任务的生命周期
    """
    # Signals
    task_added = Signal(Task)
    task_started = Signal(str)  # task_id
    task_completed = Signal(str, bytes, str, list, str, str, str)  # task_id, image_bytes, prompt, ref_images, aspect_ratio, image_size, model
    task_failed = Signal(str, str)  # task_id, error_message
    task_progress = Signal(str, float, str)  # task_id, progress, status_text

    _instance = None
    _lock = Lock()

    def __new__(cls):
        """单例模式"""
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        if hasattr(self, '_initialized'):
            return
        super().__init__()

        self.logger = logging.getLogger(__name__)
        self._tasks: Dict[str, Task] = {}
        self._queue = Queue()
        self._current_worker = None
        self._max_queue_size = 10
        self._max_history_size = 10  # 只保留最近10条完成任务

        # 加载数据库配置用于日志记录
        self._db_config = None
        self._load_db_config()

        self._initialized = True
        self.logger.info("TaskQueueManager 初始化完成")

    def _load_db_config(self):
        """加载数据库配置"""
        try:
            import json
            from pathlib import Path
            config_file = Path("config.json")
            if config_file.exists():
                with open(config_file, 'r', encoding='utf-8') as f:
                    config = json.load(f)
                    self._db_config = config.get('db_config')
                    if self._db_config:
                        self.logger.info("数据库配置已加载")
        except Exception as e:
            self.logger.warning(f"加载数据库配置失败: {e}")

    def submit_task(
        self,
        task_type: TaskType,
        prompt: str,
        api_key: str,
        reference_images: List[str],
        aspect_ratio: str,
        image_size: str,
        model: str,
        user_name: str = "",
        device_name: str = ""
    ) -> str:
        """
        提交新任务到队列

        Args:
            task_type: 任务类型
            prompt: 图片描述
            api_key: API 密钥
            reference_images: 参考图片路径列表
            aspect_ratio: 宽高比
            image_size: 图片尺寸
            model: 模型名称
            user_name: 用户名 (用于日志记录)
            device_name: 设备名称 (用于日志记录)

        Returns:
            task_id: 任务唯一标识

        Raises:
            RuntimeError: 队列已满
        """
        # 检查队列容量
        if self._queue.qsize() >= self._max_queue_size:
            raise RuntimeError(f"任务队列已满 (最大 {self._max_queue_size} 个)")

        # 创建任务
        task = Task(
            id=str(uuid.uuid4()),
            type=task_type,
            status=TaskStatus.PENDING,
            prompt=prompt,
            api_key=api_key,
            reference_images=reference_images.copy() if reference_images else [],
            aspect_ratio=aspect_ratio,
            image_size=image_size,
            model=model,
            user_name=user_name,
            device_name=device_name,
            created_at=datetime.now()
        )

        self._tasks[task.id] = task
        self._queue.put(task.id)

        self.logger.info(f"任务已提交: {task.id[:8]} - {prompt[:30]}")
        self.task_added.emit(task)

        # 如果没有正在运行的任务,启动处理
        if self._current_worker is None or not self._current_worker.isRunning():
            self._process_next()

        return task.id

    def _process_next(self):
        """处理队列中的下一个任务"""
        if self._queue.empty():
            self.logger.debug("队列为空,无任务处理")
            return

        task_id = self._queue.get()
        task = self._tasks[task_id]
        task.status = TaskStatus.RUNNING
        task.started_at = datetime.now()

        self.logger.info(f"开始处理任务: {task_id[:8]}")

        # 导入 ImageGenerationWorker
        from image_generator import ImageGenerationWorker

        # 创建 worker
        self._current_worker = ImageGenerationWorker(
            task.api_key,
            task.prompt,
            task.reference_images,
            task.aspect_ratio,
            task.image_size,
            task.model
        )

        # 绑定信号
        self._current_worker.finished.connect(
            lambda img_bytes, prompt, ref_imgs, ar, size, model:
                self._on_task_completed(task_id, img_bytes, prompt, ref_imgs, ar, size, model)
        )
        self._current_worker.error.connect(
            lambda error: self._on_task_failed(task_id, error)
        )
        self._current_worker.progress.connect(
            lambda status: self.task_progress.emit(task_id, 0.5, status)
        )

        self.task_started.emit(task_id)
        self._current_worker.start()

    def _on_task_completed(self, task_id: str, image_bytes: bytes, prompt: str,
                          reference_images: list, aspect_ratio: str, image_size: str, model: str):
        """任务完成回调"""
        task = self._tasks.get(task_id)
        if not task:
            self.logger.error(f"任务 {task_id[:8]} 不存在")
            return

        task.status = TaskStatus.COMPLETED
        task.completed_at = datetime.now()
        task.result_bytes = image_bytes

        # 生成缩略图
        try:
            task.thumbnail = self._create_thumbnail(image_bytes)
        except Exception as e:
            self.logger.warning(f"生成缩略图失败: {e}")

        elapsed = (task.completed_at - task.started_at).total_seconds()
        self.logger.info(f"任务完成: {task_id[:8]} - 耗时 {elapsed:.1f}s")

        # 记录使用日志
        self._log_usage(task_id, 'success', 'memory', None)

        self.task_completed.emit(task_id, image_bytes, prompt, reference_images,
                                aspect_ratio, image_size, model)

        # 清理旧任务历史,只保留最近的完成任务
        self._cleanup_old_tasks()

        # 处理下一个任务
        self._process_next()

    def _on_task_failed(self, task_id: str, error: str):
        """任务失败回调"""
        task = self._tasks.get(task_id)
        if not task:
            self.logger.error(f"任务 {task_id[:8]} 不存在")
            return

        task.status = TaskStatus.FAILED
        task.completed_at = datetime.now()
        task.error_message = error

        self.logger.error(f"任务失败: {task_id[:8]} - {error}")

        # 记录使用日志
        self._log_usage(task_id, 'failure', None, error)

        self.task_failed.emit(task_id, error)

        # 清理旧任务历史
        self._cleanup_old_tasks()

        # 处理下一个任务
        self._process_next()

    def _cleanup_old_tasks(self):
        """清理旧任务,只保留最近的完成/失败任务"""
        # 获取所有已完成和失败的任务,按完成时间排序
        finished_tasks = [
            t for t in self._tasks.values()
            if t.status in [TaskStatus.COMPLETED, TaskStatus.FAILED] and t.completed_at
        ]
        finished_tasks.sort(key=lambda t: t.completed_at, reverse=True)

        # 只保留最近的 N 条
        if len(finished_tasks) > self._max_history_size:
            tasks_to_remove = finished_tasks[self._max_history_size:]
            for task in tasks_to_remove:
                del self._tasks[task.id]
                self.logger.debug(f"清理旧任务: {task.id[:8]}")

    def _create_thumbnail(self, image_bytes: bytes) -> bytes:
        """
        创建缩略图 (50x50)

        Args:
            image_bytes: 原始图片字节

        Returns:
            缩略图字节
        """
        img = Image.open(io.BytesIO(image_bytes))
        img.thumbnail((50, 50))

        thumb_io = io.BytesIO()
        img.save(thumb_io, format='PNG')
        return thumb_io.getvalue()

    def get_task(self, task_id: str) -> Optional[Task]:
        """获取任务详情"""
        return self._tasks.get(task_id)

    def get_all_tasks(self) -> List[Task]:
        """获取所有任务"""
        return list(self._tasks.values())

    def get_pending_count(self) -> int:
        """获取等待中任务数"""
        return sum(1 for t in self._tasks.values() if t.status == TaskStatus.PENDING)

    def get_running_count(self) -> int:
        """获取运行中任务数"""
        return sum(1 for t in self._tasks.values() if t.status == TaskStatus.RUNNING)

    def cancel_task(self, task_id: str):
        """
        取消任务 (仅等待中任务)
        将任务状态设为 CANCELLED 并从队列中移除
        """
        task = self._tasks.get(task_id)
        if task and task.status == TaskStatus.PENDING:
            task.status = TaskStatus.CANCELLED
            task.completed_at = datetime.now()

            # 从队列中移除任务 (需要重建队列以移除特定ID)
            temp_queue = Queue()
            while not self._queue.empty():
                tid = self._queue.get()
                if tid != task_id:
                    temp_queue.put(tid)

            # 替换原队列
            self._queue = temp_queue

            self.logger.info(f"任务已取消: {task_id[:8]}")
            self.task_failed.emit(task_id, "用户取消")  # 发送取消信号以更新 UI

    def _log_usage(self, task_id: str, status: str, result_path: Optional[str], error_message: Optional[str]):
        """
        记录用户使用日志到数据库

        Args:
            task_id: 任务ID
            status: 'success' 或 'failure'
            result_path: 成功时的图片路径,失败时为 None
            error_message: 失败时的错误信息,成功时为 None
        """
        if not self._db_config:
            self.logger.debug("数据库配置未加载,跳过日志记录")
            return

        task = self._tasks.get(task_id)
        if not task:
            self.logger.warning(f"任务 {task_id[:8]} 不存在,无法记录日志")
            return

        try:
            import pymysql

            # 处理未登录用户
            user_name = task.user_name if task.user_name else "未知用户"
            device_name = task.device_name if task.device_name else "未知设备"

            # 连接数据库
            connection = pymysql.connect(
                host=self._db_config['host'],
                port=self._db_config['port'],
                user=self._db_config['user'],
                password=self._db_config['password'],
                database=self._db_config['database']
            )

            with connection.cursor() as cursor:
                sql = """
                    INSERT INTO `nano_banana_user_use_log`
                    (`user_name`, `device_name`, `prompt`, `result_path`, `status`, `error_message`)
                    VALUES (%s, %s, %s, %s, %s, %s)
                """
                cursor.execute(sql, (
                    user_name,
                    device_name,
                    task.prompt,
                    result_path,
                    status,
                    error_message
                ))

            connection.commit()
            connection.close()

            self.logger.info(f"使用日志已记录: {task_id[:8]} - {status}")

        except Exception as e:
            # 日志记录失败不应影响主流程
            self.logger.error(f"记录使用日志失败: {e}", exc_info=False)


class TaskQueueWidget(QWidget):
    """
    右侧极窄任务列表
    按设计文档显示任务状态文字列表
    """

    def __init__(self, manager: TaskQueueManager, parent=None):
        super().__init__(parent)
        self.logger = logging.getLogger(__name__)
        self.manager = manager
        self.parent_window = parent  # 用于数据回填

        self._setup_ui()
        self._connect_signals()
        self._update_summary()

    def _setup_ui(self):
        """构建右侧任务列表 UI"""
        layout = QVBoxLayout()
        layout.setContentsMargins(8, 8, 8, 8)
        layout.setSpacing(2)

        # 标题
        title = QLabel("任务队列")
        title.setStyleSheet("QLabel { font-weight: bold; font-size: 10px; color: #666; }")
        title.setAlignment(Qt.AlignCenter)
        title.setToolTip("鼠标悬停查看详情\n右键等待中的任务可取消")
        layout.addWidget(title)

        # 分隔线
        line = QLabel()
        line.setFrameStyle(QFrame.HLine | QFrame.Sunken)
        layout.addWidget(line)

        # 任务状态列表 - 可点击的列表项
        self.task_list = QListWidget()
        self.task_list.setStyleSheet("""
            QListWidget {
                border: none;
                font-size: 11px;
                padding: 2px;
            }
            QListWidget::item {
                padding: 4px 2px;
                border-bottom: 1px solid #eee;
                min-height: 20px;
            }
            QListWidget::item:hover {
                background-color: #e3f2fd;
                cursor: pointer;
            }
            QListWidget::item:selected {
                background-color: #bbdefb;
            }
        """)
        self.task_list.itemClicked.connect(self._on_task_item_clicked)

        # 启用右键菜单
        self.task_list.setContextMenuPolicy(Qt.CustomContextMenu)
        self.task_list.customContextMenuRequested.connect(self._show_context_menu)

        layout.addWidget(self.task_list)

        layout.addStretch()
        self.setLayout(layout)

        # 设置极窄宽度
        self.setMaximumWidth(120)
        self.setMinimumWidth(80)

        # 样式
        self.setStyleSheet("""
            TaskQueueWidget {
                background-color: #f5f5f5;
                border-left: 1px solid #ddd;
            }
        """)

    def _connect_signals(self):
        """绑定信号"""
        self.manager.task_added.connect(self._on_task_added)
        self.manager.task_started.connect(self._on_task_started)
        self.manager.task_completed.connect(self._on_task_completed)
        self.manager.task_failed.connect(self._on_task_failed)
        self.manager.task_progress.connect(self._on_task_progress)

    
    def _update_summary(self):
        """更新任务列表 - 显示可点击的状态项"""
        self.task_list.clear()
        tasks = self.manager.get_all_tasks()

        # 按状态分类并转换为文字
        for task in tasks:
            # 状态文字和颜色
            if task.status == TaskStatus.RUNNING:
                status_text = "执行中"
                color = "#FF9500"  # 橙色
            elif task.status == TaskStatus.PENDING:
                status_text = "等待中"
                color = "#007AFF"  # 蓝色
            elif task.status == TaskStatus.COMPLETED:
                status_text = "已完成"
                color = "#34C759"  # 绿色
            elif task.status == TaskStatus.FAILED:
                status_text = "失败"
                color = "#FF3B30"  # 红色
            else:
                status_text = "未知"
                color = "#666666"  # 灰色

            # 创建列表项
            item = QListWidgetItem(status_text)
            item.setData(Qt.UserRole, task.id)  # 存储任务ID

            # 设置颜色
            if hasattr(item, 'setForeground'):
                from PySide6.QtGui import QBrush, QColor
                item.setForeground(QBrush(QColor(color)))

            # 设置 Tooltip - 显示完整的 Prompt 和任务信息
            tooltip_text = f"Prompt: {task.prompt}\n"
            tooltip_text += f"创建时间: {task.created_at.strftime('%Y-%m-%d %H:%M:%S')}\n"
            tooltip_text += f"宽高比: {task.aspect_ratio}\n"
            tooltip_text += f"尺寸: {task.image_size}"
            if task.error_message:
                tooltip_text += f"\n错误: {task.error_message}"
            item.setToolTip(tooltip_text)

            # 添加到列表
            self.task_list.addItem(item)

        # 最多显示最近10个任务
        if self.task_list.count() > 10:
            for i in range(self.task_list.count() - 10):
                self.task_list.takeItem(0)

    def _show_context_menu(self, position):
        """显示右键菜单 - 仅为等待中的任务提供取消选项"""
        from PySide6.QtWidgets import QMenu

        # 获取点击的任务项
        item = self.task_list.itemAt(position)
        if not item:
            return

        task_id = item.data(Qt.UserRole)
        if not task_id:
            return

        task = self.manager.get_task(task_id)
        if not task:
            return

        # 仅为 PENDING 状态的任务显示取消选项
        if task.status == TaskStatus.PENDING:
            menu = QMenu()
            cancel_action = menu.addAction("取消任务")

            action = menu.exec_(self.task_list.mapToGlobal(position))

            if action == cancel_action:
                self.manager.cancel_task(task_id)
                self._update_summary()

    def _on_task_added(self, task: Task):
        """任务添加回调"""
        self._update_summary()

    def _on_task_item_clicked(self, item: QListWidgetItem):
        """点击任务项 - 回填数据到主窗口"""
        task_id = item.data(Qt.UserRole)
        if not task_id:
            return

        task = self.manager.get_task(task_id)
        if not task or not self.parent_window:
            return

        # 切换到对应的标签页
        if task.type == TaskType.STYLE_DESIGN:
            self.parent_window.tab_widget.setCurrentIndex(1)  # 款式设计标签
            style_tab = self.parent_window.tab_widget.currentWidget()
        else:
            self.parent_window.tab_widget.setCurrentIndex(0)  # 图片生成标签
            gen_tab = self.parent_window.tab_widget.currentWidget()

        # 如果是已完成任务,直接在主窗口显示结果
        if task.status == TaskStatus.COMPLETED and task.result_bytes:
            if task.type == TaskType.STYLE_DESIGN and hasattr(style_tab, '_display_generated_image_from_bytes'):
                # 款式设计:将图片数据存储到样式标签并显示
                style_tab.generated_image_bytes = task.result_bytes
                style_tab._display_generated_image_from_bytes()
            elif hasattr(gen_tab, '_display_generated_image_from_bytes'):
                # 图片生成:将图片数据存储到主窗口并显示
                self.parent_window.generated_image_bytes = task.result_bytes
                gen_tab._display_generated_image_from_bytes()

        # 回填参数到主窗口
        self._load_task_to_main_window(task)

    def _load_task_to_main_window(self, task: Task):
        """将任务数据回填到主窗口"""
        try:
            if task.type == TaskType.STYLE_DESIGN:
                # 款式设计标签页 - 回填prompt到预览框
                self.parent_window.tab_widget.setCurrentIndex(1)  # 款式设计标签
                style_tab = self.parent_window.tab_widget.currentWidget()

                # 回填prompt到预览框
                if hasattr(style_tab, 'prompt_preview'):
                    style_tab.prompt_preview.setPlainText(task.prompt)

                # 回填设置
                if hasattr(style_tab, 'aspect_ratio') and task.aspect_ratio:
                    index = style_tab.aspect_ratio.findText(task.aspect_ratio)
                    if index >= 0:
                        style_tab.aspect_ratio.setCurrentIndex(index)
                if hasattr(style_tab, 'image_size') and task.image_size:
                    index = style_tab.image_size.findText(task.image_size)
                    if index >= 0:
                        style_tab.image_size.setCurrentIndex(index)

            else:
                # 图片生成标签页
                self.parent_window.tab_widget.setCurrentIndex(0)  # 图片生成标签
                gen_tab = self.parent_window.tab_widget.currentWidget()

                # 回填prompt
                if hasattr(gen_tab, 'prompt_text'):
                    gen_tab.prompt_text.setPlainText(task.prompt)

                # 回填参考图片
                if task.reference_images and hasattr(gen_tab, 'add_reference_image'):
                    for ref_path in task.reference_images:
                        gen_tab.add_reference_image(ref_path)

                # 回填设置
                if hasattr(gen_tab, 'aspect_ratio') and task.aspect_ratio:
                    index = gen_tab.aspect_ratio.findText(task.aspect_ratio)
                    if index >= 0:
                        gen_tab.aspect_ratio.setCurrentIndex(index)
                if hasattr(gen_tab, 'image_size') and task.image_size:
                    index = gen_tab.image_size.findText(task.image_size)
                    if index >= 0:
                        gen_tab.image_size.setCurrentIndex(index)

        except Exception as e:
            self.logger.error(f"回填数据到主窗口失败: {e}")

    def _on_task_started(self, task_id: str):
        """任务开始回调"""
        self._update_summary()

    def _on_task_completed(self, task_id: str, *args):
        """任务完成回调"""
        self._update_summary()

    def _on_task_failed(self, task_id: str, error: str):
        """任务失败回调"""
        self._update_summary()

    def _show_task_detail_dialog(self):
        """显示任务详情弹窗"""
        if self.detail_dialog and self.detail_dialog.isVisible():
            self.detail_dialog.raise_()
            return

        dialog = QDialog(self)
        dialog.setWindowTitle("任务详情")
        dialog.resize(400, 500)

        layout = QVBoxLayout()

        # 标题
        title = QLabel("📋 任务队列")
        title.setStyleSheet("QLabel { font-weight: bold; font-size: 14px; }")
        layout.addWidget(title)

        # 任务列表
        task_list = QListWidget()
        task_list.itemClicked.connect(lambda item: self._on_detail_task_clicked(item, dialog))

        # 添加任务项
        for task in self.manager.get_all_tasks():
            item = QListWidgetItem()
            item.setData(Qt.UserRole, task.id)

            status_map = {
                TaskStatus.RUNNING: ("●", "#FF9500"),
                TaskStatus.PENDING: ("○", "#8E8E93"),
                TaskStatus.COMPLETED: ("✓", "#34C759"),
                TaskStatus.FAILED: ("✗", "#FF3B30"),
            }
            icon, color = status_map.get(task.status, ("?", "#000"))

            prompt_preview = task.prompt[:30] + "..." if len(task.prompt) > 30 else task.prompt
            display_text = f"{icon} {prompt_preview}"

            item.setText(display_text)
            if hasattr(item, 'setForeground'):
                from PySide6.QtGui import QBrush, QColor
                item.setForeground(QBrush(QColor(color)))

            task_list.addItem(item)

        layout.addWidget(task_list)

        # 关闭按钮
        close_btn = QPushButton("关闭")
        close_btn.clicked.connect(dialog.close)
        layout.addWidget(close_btn)

        dialog.setLayout(layout)
        self.detail_dialog = dialog
        dialog.exec()

    def _on_detail_task_clicked(self, item: QListWidgetItem, dialog: QDialog):
        """详情弹窗中点击任务项"""
        dialog.close()
        self._on_task_item_clicked(item)

    def _on_task_progress(self, task_id: str, progress: float, status_text: str):
        """任务进度回调"""
        # 侧边栏模式下进度信息通过状态标签显示
        # 不需要额外更新,由_update_summary统一处理
        pass

    
    def _on_task_item_clicked(self, item: QListWidgetItem):
        """单击任务 - 回填数据到主窗口或显示结果"""
        task_id = item.data(Qt.UserRole)
        task = self.manager.get_task(task_id)

        if not task or not self.parent_window:
            return

        # 切换到对应的标签页
        if task.type == TaskType.STYLE_DESIGN:
            self.parent_window.tab_widget.setCurrentIndex(1)  # 款式设计标签
            style_tab = self.parent_window.tab_widget.currentWidget()
        else:
            self.parent_window.tab_widget.setCurrentIndex(0)  # 图片生成标签
            gen_tab = self.parent_window.tab_widget.currentWidget()

        # 如果是已完成任务,直接在主窗口显示结果
        if task.status == TaskStatus.COMPLETED and task.result_bytes:
            self.parent_window.generated_image_bytes = task.result_bytes
            # 显示生成的图片
            if hasattr(self.parent_window, 'display_generated_image'):
                self.parent_window.display_generated_image()
            elif task.type == TaskType.STYLE_DESIGN and hasattr(style_tab, '_display_generated_image_from_bytes'):
                style_tab._display_generated_image_from_bytes()
            elif hasattr(gen_tab, '_display_generated_image_from_bytes'):
                gen_tab._display_generated_image_from_bytes()

        # 回填参数
        self._load_task_to_main_window(task)

    def _load_task_to_main_window(self, task: Task):
        """将任务数据回填到主窗口"""
        try:
            if task.type == TaskType.STYLE_DESIGN:
                # 切换到款式设计标签
                self.parent_window.tab_widget.setCurrentIndex(1)
                style_tab = self.parent_window.tab_widget.currentWidget()
                if hasattr(style_tab, 'library_manager'):
                    # 款式设计不需要回填prompt,因为是参数组合
                    pass
            else:
                # 切换到图片生成标签
                self.parent_window.tab_widget.setCurrentIndex(0)
                gen_tab = self.parent_window.tab_widget.currentWidget()
                if hasattr(gen_tab, 'prompt_input'):
                    # 回填prompt
                    gen_tab.prompt_input.setPlainText(task.prompt)
                    # 回填参考图片
                    if task.reference_images:
                        for ref_path in task.reference_images:
                            if hasattr(gen_tab, 'add_reference_image'):
                                gen_tab.add_reference_image(ref_path)
                    # 回填设置
                    if hasattr(gen_tab, 'aspect_ratio') and task.aspect_ratio:
                        index = gen_tab.aspect_ratio.findText(task.aspect_ratio)
                        if index >= 0:
                            gen_tab.aspect_ratio.setCurrentIndex(index)
                    if hasattr(gen_tab, 'image_size') and task.image_size:
                        index = gen_tab.image_size.findText(task.image_size)
                        if index >= 0:
                            gen_tab.image_size.setCurrentIndex(index)

        except Exception as e:
            self.logger.error(f"回填数据到主窗口失败: {e}")