ae36efe3 by 柴进

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>
1 parent a442e36d
...@@ -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 # 审计元信息
......