fix(sidebar): 补 TaskQueueWidget 漏掉的 loadTask + 右键菜单
承认上次盘点不全 — 只看了 image_generator.py,没看 task_queue.py 的
TaskQueueWidget,这两个旧功能漏了:
# 1. 左键任务项 → 回填到对应 tab(旧 _on_task_item_clicked + _load_task_to_main_window)
task_queue.py:
Task 加 result_path 字段,已完成任务保留生成图绝对路径供 sidebar 回显
bridges/imagegen.py:
- submitTask 拆出 _do_submit;新增 submitStyleTask(task_type=STYLE_DESIGN)
StyleDesignerTab.submit 改调 submitStyleTask,让 sidebar 知道任务来源
- _on_completed 写回 task.result_path(=生成图 history 路径),不依赖 bytes
bridges/taskqueue.py:
+ Signal taskLoadRequested(payload)
+ Slot loadTask(task_id) → 拿 task → 反查 mode(model_id → 极速/慢速)
+ 过滤已删参考图 + 已完成才带 resultPath → emit payload
qml_poc/qml/MainWindow.qml:
sidebar delegate 左键 → taskQueue.loadTask;右键 → 弹 Menu(不直接取消)
qml_poc/qml/ImageGenTab.qml:
Connections 接 taskQueue.taskLoadRequested,type=image_gen 时回填
prompt / refImages / aspect / size / mode / resultPath,并切到 tab 0
qml_poc/qml/StyleDesignerTab.qml:
Connections 接同信号,type=style_design 时回填 assembledPrompt + resultPath,
切到 tab 1(注:8 字段 ComboBox 无法反序列化回填,旧版 prompt_preview 行为一致)
# 2. 右键弹菜单(旧 _show_context_menu,不是直接取消)
之前一版我做的"右键直接取消"是错的。改成右键 popup Menu,包含 "取消任务" /
"取消任务(运行中)" MenuItem,用户点击 MenuItem 才真取消。
status 不是 pending/running 时 MenuItem disabled。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Showing
6 changed files
with
171 additions
and
15 deletions
| ... | @@ -126,20 +126,32 @@ class ImageGenBridge(QObject): | ... | @@ -126,20 +126,32 @@ class ImageGenBridge(QObject): |
| 126 | @Slot(str, list, str, str, str, result=str) | 126 | @Slot(str, list, str, str, str, result=str) |
| 127 | def submitTask(self, prompt: str, reference_images: list, | 127 | def submitTask(self, prompt: str, reference_images: list, |
| 128 | aspect_ratio: str, image_size: str, mode: str) -> str: | 128 | aspect_ratio: str, image_size: str, mode: str) -> str: |
| 129 | """提交一条生成任务,返回 task_id。失败抛 RuntimeError。 | 129 | """图片生成 tab 提交(task_type=IMAGE_GENERATION)。""" |
| 130 | 130 | return self._do_submit( | |
| 131 | Args: | 131 | prompt, reference_images, aspect_ratio, image_size, mode, |
| 132 | prompt: 中文提示词 | 132 | is_style_design=False, |
| 133 | reference_images: 参考图本地路径 list[str] | 133 | ) |
| 134 | aspect_ratio: '1:1' / '2:3' / ... | 134 | |
| 135 | image_size: '1K' / '2K' / '4K' | 135 | @Slot(str, list, str, str, str, result=str) |
| 136 | mode: '极速模式' 或 '慢速模式' | 136 | def submitStyleTask(self, prompt: str, reference_images: list, |
| 137 | aspect_ratio: str, image_size: str, mode: str) -> str: | ||
| 138 | """款式设计 tab 提交(task_type=STYLE_DESIGN)。 | ||
| 139 | |||
| 140 | 与 submitTask 行为一致,仅 task.type 不同 — sidebar 点击已完成任务时 | ||
| 141 | TaskQueueBridge.loadTask 据此分发回填到正确的 tab。 | ||
| 137 | """ | 142 | """ |
| 143 | return self._do_submit( | ||
| 144 | prompt, reference_images, aspect_ratio, image_size, mode, | ||
| 145 | is_style_design=True, | ||
| 146 | ) | ||
| 147 | |||
| 148 | def _do_submit(self, prompt, reference_images, aspect_ratio, image_size, mode, | ||
| 149 | is_style_design: bool) -> str: | ||
| 138 | from task_queue import TaskType # 局部 import 避免桥层冷启动加载 Qt UI | 150 | from task_queue import TaskType # 局部 import 避免桥层冷启动加载 Qt UI |
| 139 | 151 | ||
| 140 | model = MODEL_BY_MODE.get(mode, MODEL_PRO) | 152 | model = MODEL_BY_MODE.get(mode, MODEL_PRO) |
| 141 | task_id = self._tqm.submit_task( | 153 | task_id = self._tqm.submit_task( |
| 142 | task_type=TaskType.IMAGE_GENERATION, | 154 | task_type=TaskType.STYLE_DESIGN if is_style_design else TaskType.IMAGE_GENERATION, |
| 143 | prompt=prompt, | 155 | prompt=prompt, |
| 144 | api_key=self._api_key, | 156 | api_key=self._api_key, |
| 145 | reference_images=list(reference_images or []), | 157 | reference_images=list(reference_images or []), |
| ... | @@ -149,7 +161,10 @@ class ImageGenBridge(QObject): | ... | @@ -149,7 +161,10 @@ class ImageGenBridge(QObject): |
| 149 | user_name=self._auth.currentUser if self._auth else "", | 161 | user_name=self._auth.currentUser if self._auth else "", |
| 150 | device_name=self._auth.deviceName() if self._auth else "", | 162 | device_name=self._auth.deviceName() if self._auth else "", |
| 151 | ) | 163 | ) |
| 152 | self._logger.info(f"提交生成任务: {task_id[:8]} - mode={mode}") | 164 | self._logger.info( |
| 165 | f"提交{'款式设计' if is_style_design else '图片生成'}任务: " | ||
| 166 | f"{task_id[:8]} - mode={mode}" | ||
| 167 | ) | ||
| 153 | return task_id | 168 | return task_id |
| 154 | 169 | ||
| 155 | @Slot(str) | 170 | @Slot(str) |
| ... | @@ -480,11 +495,19 @@ class ImageGenBridge(QObject): | ... | @@ -480,11 +495,19 @@ class ImageGenBridge(QObject): |
| 480 | ) | 495 | ) |
| 481 | result_path = str(self._history.base_path / timestamp / "generated.png") | 496 | result_path = str(self._history.base_path / timestamp / "generated.png") |
| 482 | except Exception as e: | 497 | except Exception as e: |
| 483 | self._logger.error(f"保存历史失败 {task_id[:8]}: {e}", exc_info=True) | 498 | self._logger.exception(f"保存历史失败 {task_id[:8]}") |
| 484 | self.taskFailed.emit(task_id, f"图片生成成功但保存历史失败: {e}") | 499 | self.taskFailed.emit(task_id, f"图片生成成功但保存历史失败: {e}") |
| 485 | self.busyChanged.emit() | 500 | self.busyChanged.emit() |
| 486 | return | 501 | return |
| 487 | 502 | ||
| 503 | # 把 result_path 写回 Task,让 TaskQueueBridge.loadTask 能拿到回显路径 | ||
| 504 | try: | ||
| 505 | task = self._tqm.get_task(task_id) | ||
| 506 | if task is not None: | ||
| 507 | task.result_path = Path(result_path).as_posix() | ||
| 508 | except Exception: | ||
| 509 | self._logger.exception(f"task.result_path 写回失败 {task_id[:8]}") | ||
| 510 | |||
| 488 | self.taskCompleted.emit(task_id, result_path, prompt, model) | 511 | self.taskCompleted.emit(task_id, result_path, prompt, model) |
| 489 | self.busyChanged.emit() | 512 | self.busyChanged.emit() |
| 490 | 513 | ... | ... |
| ... | @@ -8,12 +8,15 @@ QML sidebar ListView 直接用 taskQueue.model(QAbstractListModel), | ... | @@ -8,12 +8,15 @@ QML sidebar ListView 直接用 taskQueue.model(QAbstractListModel), |
| 8 | """ | 8 | """ |
| 9 | import logging | 9 | import logging |
| 10 | from datetime import datetime | 10 | from datetime import datetime |
| 11 | from pathlib import Path | ||
| 11 | from typing import Dict, List | 12 | from typing import Dict, List |
| 12 | 13 | ||
| 13 | from PySide6.QtCore import ( | 14 | from PySide6.QtCore import ( |
| 14 | Property, QAbstractListModel, QModelIndex, QObject, Qt, Signal, Slot, | 15 | Property, QAbstractListModel, QModelIndex, QObject, Qt, Signal, Slot, |
| 15 | ) | 16 | ) |
| 16 | 17 | ||
| 18 | from core.generation import MODEL_BY_MODE | ||
| 19 | |||
| 17 | 20 | ||
| 18 | class _TaskListModel(QAbstractListModel): | 21 | class _TaskListModel(QAbstractListModel): |
| 19 | """暴露给 QML ListView 的任务列表模型。 | 22 | """暴露给 QML ListView 的任务列表模型。 |
| ... | @@ -86,6 +89,8 @@ class _TaskListModel(QAbstractListModel): | ... | @@ -86,6 +89,8 @@ class _TaskListModel(QAbstractListModel): |
| 86 | class TaskQueueBridge(QObject): | 89 | class TaskQueueBridge(QObject): |
| 87 | pendingCountChanged = Signal() | 90 | pendingCountChanged = Signal() |
| 88 | runningCountChanged = Signal() | 91 | runningCountChanged = Signal() |
| 92 | # sidebar 点击任务项 → 让目标 tab 回填 prompt/参考图/设置/结果图(旧 _load_task_to_main_window 等价) | ||
| 93 | taskLoadRequested = Signal("QVariantMap") | ||
| 89 | 94 | ||
| 90 | def __init__(self, task_queue_manager, parent=None): | 95 | def __init__(self, task_queue_manager, parent=None): |
| 91 | super().__init__(parent) | 96 | super().__init__(parent) |
| ... | @@ -121,6 +126,60 @@ class TaskQueueBridge(QObject): | ... | @@ -121,6 +126,60 @@ class TaskQueueBridge(QObject): |
| 121 | self.pendingCountChanged.emit() | 126 | self.pendingCountChanged.emit() |
| 122 | self.runningCountChanged.emit() | 127 | self.runningCountChanged.emit() |
| 123 | 128 | ||
| 129 | @Slot(str) | ||
| 130 | def loadTask(self, task_id: str) -> None: | ||
| 131 | """点击 sidebar 任务项 → 发 taskLoadRequested 信号,让对应 tab 回填字段。 | ||
| 132 | |||
| 133 | payload 字段(QML 友好的 dict): | ||
| 134 | taskId, type ("image_gen" | "style_design"), | ||
| 135 | prompt, referenceImages (list[str], 已过滤掉磁盘失效路径), | ||
| 136 | aspectRatio, imageSize, mode ("极速模式" | "慢速模式"), | ||
| 137 | resultPath (str, 仅已完成任务有;空字符串表示未完成 / 失败 / 取消) | ||
| 138 | """ | ||
| 139 | from task_queue import TaskStatus, TaskType | ||
| 140 | task = self._tqm.get_task(task_id) | ||
| 141 | if task is None: | ||
| 142 | self._logger.warning(f"loadTask: 任务不存在 {task_id[:8]}") | ||
| 143 | return | ||
| 144 | |||
| 145 | type_str = "style_design" if task.type == TaskType.STYLE_DESIGN else "image_gen" | ||
| 146 | |||
| 147 | # model_id → mode 中文名(生成时记 model_id,回填要还原 ComboBox 文字) | ||
| 148 | mode = "慢速模式" | ||
| 149 | for k, v in MODEL_BY_MODE.items(): | ||
| 150 | if v == task.model: | ||
| 151 | mode = k | ||
| 152 | break | ||
| 153 | |||
| 154 | # 只保留磁盘上仍存在的参考图路径(旧任务可能引用已删文件) | ||
| 155 | valid_refs = [] | ||
| 156 | for p in (task.reference_images or []): | ||
| 157 | if not p: | ||
| 158 | continue | ||
| 159 | try: | ||
| 160 | if Path(p).exists(): | ||
| 161 | valid_refs.append(Path(p).as_posix()) | ||
| 162 | except Exception: | ||
| 163 | continue | ||
| 164 | |||
| 165 | result_path = task.result_path if task.status == TaskStatus.COMPLETED else "" | ||
| 166 | |||
| 167 | payload = { | ||
| 168 | "taskId": task_id, | ||
| 169 | "type": type_str, | ||
| 170 | "prompt": task.prompt or "", | ||
| 171 | "referenceImages": valid_refs, | ||
| 172 | "aspectRatio": task.aspect_ratio or "", | ||
| 173 | "imageSize": task.image_size or "", | ||
| 174 | "mode": mode, | ||
| 175 | "resultPath": result_path or "", | ||
| 176 | } | ||
| 177 | self._logger.info( | ||
| 178 | f"loadTask emit: {task_id[:8]} type={type_str} " | ||
| 179 | f"refs={len(valid_refs)} hasResult={bool(result_path)}" | ||
| 180 | ) | ||
| 181 | self.taskLoadRequested.emit(payload) | ||
| 182 | |||
| 124 | # ---- 信号转 model 增量 ----------------------------------------------- | 183 | # ---- 信号转 model 增量 ----------------------------------------------- |
| 125 | 184 | ||
| 126 | def _on_task_added(self, task) -> None: | 185 | def _on_task_added(self, task) -> None: | ... | ... |
| ... | @@ -149,6 +149,33 @@ Item { | ... | @@ -149,6 +149,33 @@ Item { |
| 149 | } | 149 | } |
| 150 | } | 150 | } |
| 151 | 151 | ||
| 152 | // sidebar 点击任务项 → 回填到本 tab(旧 _load_task_to_main_window 等价) | ||
| 153 | Connections { | ||
| 154 | target: taskQueue | ||
| 155 | function onTaskLoadRequested(payload) { | ||
| 156 | if (!payload || payload.type !== "image_gen") return | ||
| 157 | appState.setTab(0) // 切到图片生成 tab | ||
| 158 | |||
| 159 | promptArea.text = payload.prompt || "" | ||
| 160 | tab.refImages = (payload.referenceImages || []).slice() | ||
| 161 | |||
| 162 | var ai = aspectCombo.find(payload.aspectRatio || "") | ||
| 163 | if (ai >= 0) aspectCombo.currentIndex = ai | ||
| 164 | var si = sizeCombo.find(payload.imageSize || "") | ||
| 165 | if (si >= 0) sizeCombo.currentIndex = si | ||
| 166 | var mi = modeCombo.find(payload.mode || "") | ||
| 167 | if (mi >= 0) modeCombo.currentIndex = mi | ||
| 168 | |||
| 169 | // 已完成任务回显结果图(payload.resultPath 仅 COMPLETED 时有) | ||
| 170 | if (payload.resultPath) { | ||
| 171 | tab.lastResultPath = payload.resultPath | ||
| 172 | } | ||
| 173 | |||
| 174 | tab.statusText = "● 已加载任务 " + (payload.taskId || "").substring(0, 8) | ||
| 175 | tab.statusColor = App.Theme.accent | ||
| 176 | } | ||
| 177 | } | ||
| 178 | |||
| 152 | // ===== 桥层信号 ===== | 179 | // ===== 桥层信号 ===== |
| 153 | Connections { | 180 | Connections { |
| 154 | target: imageGen | 181 | target: imageGen | ... | ... |
| ... | @@ -172,6 +172,23 @@ Rectangle { | ... | @@ -172,6 +172,23 @@ Rectangle { |
| 172 | 172 | ||
| 173 | model: taskQueue.model | 173 | model: taskQueue.model |
| 174 | 174 | ||
| 175 | // 右键菜单上下文:当前点击的任务 id + 状态 | ||
| 176 | property string ctxTaskId: "" | ||
| 177 | property string ctxStatus: "" | ||
| 178 | |||
| 179 | Menu { | ||
| 180 | id: taskCtxMenu | ||
| 181 | MenuItem { | ||
| 182 | text: taskList.ctxStatus === "pending" ? "取消任务" | ||
| 183 | : taskList.ctxStatus === "running" ? "取消任务(运行中)" | ||
| 184 | : "取消任务" | ||
| 185 | enabled: taskList.ctxStatus === "pending" || taskList.ctxStatus === "running" | ||
| 186 | onTriggered: { | ||
| 187 | if (taskList.ctxTaskId) taskQueue.cancelTask(taskList.ctxTaskId) | ||
| 188 | } | ||
| 189 | } | ||
| 190 | } | ||
| 191 | |||
| 175 | // 空状态占位 | 192 | // 空状态占位 |
| 176 | Label { | 193 | Label { |
| 177 | anchors.centerIn: parent | 194 | anchors.centerIn: parent |
| ... | @@ -261,9 +278,14 @@ Rectangle { | ... | @@ -261,9 +278,14 @@ Rectangle { |
| 261 | cursorShape: Qt.PointingHandCursor | 278 | cursorShape: Qt.PointingHandCursor |
| 262 | acceptedButtons: Qt.LeftButton | Qt.RightButton | 279 | acceptedButtons: Qt.LeftButton | Qt.RightButton |
| 263 | onClicked: function(mouse) { | 280 | onClicked: function(mouse) { |
| 264 | if (mouse.button === Qt.RightButton | 281 | if (mouse.button === Qt.RightButton) { |
| 265 | && (status === "pending" || status === "running")) { | 282 | // 右键弹菜单(旧 _show_context_menu 等价),不直接取消 |
| 266 | taskQueue.cancelTask(taskId) | 283 | taskList.ctxTaskId = taskId |
| 284 | taskList.ctxStatus = status | ||
| 285 | taskCtxMenu.popup() | ||
| 286 | } else { | ||
| 287 | // 左键:回填到对应 tab(旧 _load_task_to_main_window 等价) | ||
| 288 | taskQueue.loadTask(taskId) | ||
| 267 | } | 289 | } |
| 268 | } | 290 | } |
| 269 | } | 291 | } | ... | ... |
| ... | @@ -93,7 +93,9 @@ Item { | ... | @@ -93,7 +93,9 @@ Item { |
| 93 | return | 93 | return |
| 94 | } | 94 | } |
| 95 | try { | 95 | try { |
| 96 | var taskId = imageGen.submitTask( | 96 | // submitStyleTask 而非 submitTask:标记 task.type=STYLE_DESIGN, |
| 97 | // sidebar 点击该任务时 TaskQueueBridge.loadTask 才知道回填到款式设计 tab | ||
| 98 | var taskId = imageGen.submitStyleTask( | ||
| 97 | tab.assembledPrompt, [], "1:1", "2K", "慢速模式" | 99 | tab.assembledPrompt, [], "1:1", "2K", "慢速模式" |
| 98 | ) | 100 | ) |
| 99 | tab.myTaskIds = tab.myTaskIds.concat([taskId]) | 101 | tab.myTaskIds = tab.myTaskIds.concat([taskId]) |
| ... | @@ -118,6 +120,26 @@ Item { | ... | @@ -118,6 +120,26 @@ Item { |
| 118 | } | 120 | } |
| 119 | } | 121 | } |
| 120 | 122 | ||
| 123 | // sidebar 点击任务项 → 回填到本 tab(旧 _load_task_to_main_window 等价) | ||
| 124 | // 注意:款式设计的 prompt 是 PromptAssembler 拼出的长串,无法反向解析回 8 字段, | ||
| 125 | // 只回填 assembledPrompt 显示 + 已完成的结果图(与旧版 prompt_preview 行为一致) | ||
| 126 | Connections { | ||
| 127 | target: taskQueue | ||
| 128 | function onTaskLoadRequested(payload) { | ||
| 129 | if (!payload || payload.type !== "style_design") return | ||
| 130 | appState.setTab(1) // 切到款式设计 tab | ||
| 131 | |||
| 132 | tab.assembledPrompt = payload.prompt || "" | ||
| 133 | |||
| 134 | if (payload.resultPath) { | ||
| 135 | tab.lastResultPath = payload.resultPath | ||
| 136 | } | ||
| 137 | |||
| 138 | tab.statusText = "● 已加载任务 " + (payload.taskId || "").substring(0, 8) | ||
| 139 | tab.statusColor = App.Theme.accent | ||
| 140 | } | ||
| 141 | } | ||
| 142 | |||
| 121 | Connections { | 143 | Connections { |
| 122 | target: imageGen | 144 | target: imageGen |
| 123 | function onTaskProgress(taskId, progress, msg) { | 145 | function onTaskProgress(taskId, progress, msg) { | ... | ... |
| ... | @@ -64,6 +64,9 @@ class Task: | ... | @@ -64,6 +64,9 @@ class Task: |
| 64 | 64 | ||
| 65 | # 结果 | 65 | # 结果 |
| 66 | result_bytes: Optional[bytes] = None | 66 | result_bytes: Optional[bytes] = None |
| 67 | # 历史记录里的生成图绝对路径(ImageGenBridge._on_completed 落 history 后 set); | ||
| 68 | # 用于 sidebar 点击已完成任务时回填预览,避免 QML 处理 bytes | ||
| 69 | result_path: Optional[str] = None | ||
| 67 | error_message: Optional[str] = None | 70 | error_message: Optional[str] = None |
| 68 | 71 | ||
| 69 | # 审计元信息 | 72 | # 审计元信息 | ... | ... |
-
Please register or sign in to post a comment