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):
@Slot(str, list, str, str, str, result=str)
def submitTask(self, prompt: str, reference_images: list,
aspect_ratio: str, image_size: str, mode: str) -> str:
"""提交一条生成任务,返回 task_id。失败抛 RuntimeError。
Args:
prompt: 中文提示词
reference_images: 参考图本地路径 list[str]
aspect_ratio: '1:1' / '2:3' / ...
image_size: '1K' / '2K' / '4K'
mode: '极速模式' 或 '慢速模式'
"""图片生成 tab 提交(task_type=IMAGE_GENERATION)。"""
return self._do_submit(
prompt, reference_images, aspect_ratio, image_size, mode,
is_style_design=False,
)
@Slot(str, list, str, str, str, result=str)
def submitStyleTask(self, prompt: str, reference_images: list,
aspect_ratio: str, image_size: str, mode: str) -> str:
"""款式设计 tab 提交(task_type=STYLE_DESIGN)。
与 submitTask 行为一致,仅 task.type 不同 — sidebar 点击已完成任务时
TaskQueueBridge.loadTask 据此分发回填到正确的 tab。
"""
return self._do_submit(
prompt, reference_images, aspect_ratio, image_size, mode,
is_style_design=True,
)
def _do_submit(self, prompt, reference_images, aspect_ratio, image_size, mode,
is_style_design: bool) -> str:
from task_queue import TaskType # 局部 import 避免桥层冷启动加载 Qt UI
model = MODEL_BY_MODE.get(mode, MODEL_PRO)
task_id = self._tqm.submit_task(
task_type=TaskType.IMAGE_GENERATION,
task_type=TaskType.STYLE_DESIGN if is_style_design else TaskType.IMAGE_GENERATION,
prompt=prompt,
api_key=self._api_key,
reference_images=list(reference_images or []),
......@@ -149,7 +161,10 @@ class ImageGenBridge(QObject):
user_name=self._auth.currentUser if self._auth else "",
device_name=self._auth.deviceName() if self._auth else "",
)
self._logger.info(f"提交生成任务: {task_id[:8]} - mode={mode}")
self._logger.info(
f"提交{'款式设计' if is_style_design else '图片生成'}任务: "
f"{task_id[:8]} - mode={mode}"
)
return task_id
@Slot(str)
......@@ -480,11 +495,19 @@ class ImageGenBridge(QObject):
)
result_path = str(self._history.base_path / timestamp / "generated.png")
except Exception as e:
self._logger.error(f"保存历史失败 {task_id[:8]}: {e}", exc_info=True)
self._logger.exception(f"保存历史失败 {task_id[:8]}")
self.taskFailed.emit(task_id, f"图片生成成功但保存历史失败: {e}")
self.busyChanged.emit()
return
# 把 result_path 写回 Task,让 TaskQueueBridge.loadTask 能拿到回显路径
try:
task = self._tqm.get_task(task_id)
if task is not None:
task.result_path = Path(result_path).as_posix()
except Exception:
self._logger.exception(f"task.result_path 写回失败 {task_id[:8]}")
self.taskCompleted.emit(task_id, result_path, prompt, model)
self.busyChanged.emit()
......
......@@ -8,12 +8,15 @@ QML sidebar ListView 直接用 taskQueue.model(QAbstractListModel),
"""
import logging
from datetime import datetime
from pathlib import Path
from typing import Dict, List
from PySide6.QtCore import (
Property, QAbstractListModel, QModelIndex, QObject, Qt, Signal, Slot,
)
from core.generation import MODEL_BY_MODE
class _TaskListModel(QAbstractListModel):
"""暴露给 QML ListView 的任务列表模型。
......@@ -86,6 +89,8 @@ class _TaskListModel(QAbstractListModel):
class TaskQueueBridge(QObject):
pendingCountChanged = Signal()
runningCountChanged = Signal()
# sidebar 点击任务项 → 让目标 tab 回填 prompt/参考图/设置/结果图(旧 _load_task_to_main_window 等价)
taskLoadRequested = Signal("QVariantMap")
def __init__(self, task_queue_manager, parent=None):
super().__init__(parent)
......@@ -121,6 +126,60 @@ class TaskQueueBridge(QObject):
self.pendingCountChanged.emit()
self.runningCountChanged.emit()
@Slot(str)
def loadTask(self, task_id: str) -> None:
"""点击 sidebar 任务项 → 发 taskLoadRequested 信号,让对应 tab 回填字段。
payload 字段(QML 友好的 dict):
taskId, type ("image_gen" | "style_design"),
prompt, referenceImages (list[str], 已过滤掉磁盘失效路径),
aspectRatio, imageSize, mode ("极速模式" | "慢速模式"),
resultPath (str, 仅已完成任务有;空字符串表示未完成 / 失败 / 取消)
"""
from task_queue import TaskStatus, TaskType
task = self._tqm.get_task(task_id)
if task is None:
self._logger.warning(f"loadTask: 任务不存在 {task_id[:8]}")
return
type_str = "style_design" if task.type == TaskType.STYLE_DESIGN else "image_gen"
# model_id → mode 中文名(生成时记 model_id,回填要还原 ComboBox 文字)
mode = "慢速模式"
for k, v in MODEL_BY_MODE.items():
if v == task.model:
mode = k
break
# 只保留磁盘上仍存在的参考图路径(旧任务可能引用已删文件)
valid_refs = []
for p in (task.reference_images or []):
if not p:
continue
try:
if Path(p).exists():
valid_refs.append(Path(p).as_posix())
except Exception:
continue
result_path = task.result_path if task.status == TaskStatus.COMPLETED else ""
payload = {
"taskId": task_id,
"type": type_str,
"prompt": task.prompt or "",
"referenceImages": valid_refs,
"aspectRatio": task.aspect_ratio or "",
"imageSize": task.image_size or "",
"mode": mode,
"resultPath": result_path or "",
}
self._logger.info(
f"loadTask emit: {task_id[:8]} type={type_str} "
f"refs={len(valid_refs)} hasResult={bool(result_path)}"
)
self.taskLoadRequested.emit(payload)
# ---- 信号转 model 增量 -----------------------------------------------
def _on_task_added(self, task) -> None:
......
......@@ -149,6 +149,33 @@ Item {
}
}
// sidebar 点击任务项 → 回填到本 tab(旧 _load_task_to_main_window 等价)
Connections {
target: taskQueue
function onTaskLoadRequested(payload) {
if (!payload || payload.type !== "image_gen") return
appState.setTab(0) // 切到图片生成 tab
promptArea.text = payload.prompt || ""
tab.refImages = (payload.referenceImages || []).slice()
var ai = aspectCombo.find(payload.aspectRatio || "")
if (ai >= 0) aspectCombo.currentIndex = ai
var si = sizeCombo.find(payload.imageSize || "")
if (si >= 0) sizeCombo.currentIndex = si
var mi = modeCombo.find(payload.mode || "")
if (mi >= 0) modeCombo.currentIndex = mi
// 已完成任务回显结果图(payload.resultPath 仅 COMPLETED 时有)
if (payload.resultPath) {
tab.lastResultPath = payload.resultPath
}
tab.statusText = "● 已加载任务 " + (payload.taskId || "").substring(0, 8)
tab.statusColor = App.Theme.accent
}
}
// ===== 桥层信号 =====
Connections {
target: imageGen
......
......@@ -172,6 +172,23 @@ Rectangle {
model: taskQueue.model
// 右键菜单上下文:当前点击的任务 id + 状态
property string ctxTaskId: ""
property string ctxStatus: ""
Menu {
id: taskCtxMenu
MenuItem {
text: taskList.ctxStatus === "pending" ? "取消任务"
: taskList.ctxStatus === "running" ? "取消任务(运行中)"
: "取消任务"
enabled: taskList.ctxStatus === "pending" || taskList.ctxStatus === "running"
onTriggered: {
if (taskList.ctxTaskId) taskQueue.cancelTask(taskList.ctxTaskId)
}
}
}
// 空状态占位
Label {
anchors.centerIn: parent
......@@ -261,9 +278,14 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: function(mouse) {
if (mouse.button === Qt.RightButton
&& (status === "pending" || status === "running")) {
taskQueue.cancelTask(taskId)
if (mouse.button === Qt.RightButton) {
// 右键弹菜单(旧 _show_context_menu 等价),不直接取消
taskList.ctxTaskId = taskId
taskList.ctxStatus = status
taskCtxMenu.popup()
} else {
// 左键:回填到对应 tab(旧 _load_task_to_main_window 等价)
taskQueue.loadTask(taskId)
}
}
}
......
......@@ -93,7 +93,9 @@ Item {
return
}
try {
var taskId = imageGen.submitTask(
// submitStyleTask 而非 submitTask:标记 task.type=STYLE_DESIGN,
// sidebar 点击该任务时 TaskQueueBridge.loadTask 才知道回填到款式设计 tab
var taskId = imageGen.submitStyleTask(
tab.assembledPrompt, [], "1:1", "2K", "慢速模式"
)
tab.myTaskIds = tab.myTaskIds.concat([taskId])
......@@ -118,6 +120,26 @@ Item {
}
}
// sidebar 点击任务项 → 回填到本 tab(旧 _load_task_to_main_window 等价)
// 注意:款式设计的 prompt 是 PromptAssembler 拼出的长串,无法反向解析回 8 字段,
// 只回填 assembledPrompt 显示 + 已完成的结果图(与旧版 prompt_preview 行为一致)
Connections {
target: taskQueue
function onTaskLoadRequested(payload) {
if (!payload || payload.type !== "style_design") return
appState.setTab(1) // 切到款式设计 tab
tab.assembledPrompt = payload.prompt || ""
if (payload.resultPath) {
tab.lastResultPath = payload.resultPath
}
tab.statusText = "● 已加载任务 " + (payload.taskId || "").substring(0, 8)
tab.statusColor = App.Theme.accent
}
}
Connections {
target: imageGen
function onTaskProgress(taskId, progress, msg) {
......
......@@ -64,6 +64,9 @@ class Task:
# 结果
result_bytes: Optional[bytes] = None
# 历史记录里的生成图绝对路径(ImageGenBridge._on_completed 落 history 后 set);
# 用于 sidebar 点击已完成任务时回填预览,避免 QML 处理 bytes
result_path: Optional[str] = None
error_message: Optional[str] = None
# 审计元信息
......