feat: complete T-534 generated reset enhancements

This commit is contained in:
chengma
2026-07-06 20:35:47 +08:00
parent 46995b301d
commit 71f8beae57
9 changed files with 488 additions and 63 deletions
+5 -1
View File
@@ -247,7 +247,11 @@ def generation_needs(task, generate_cover=False):
return {"title": False, "cover": False}
if stage not in {"collected", "generated"}:
return {"title": False, "cover": False}
if stage == "generated" and _task_attempt_count(task, "apply_attempts") > 0:
if (
stage == "generated"
and status == "failed"
and _task_attempt_count(task, "apply_attempts") > 0
):
return {"title": False, "cover": False}
needs_title = not _has_generated_title(task)
needs_cover = bool(generate_cover) and not _has_generated_cover(task)
+24 -9
View File
@@ -743,32 +743,45 @@ def set_applied(task_id, committed, error=None, path=None, conn=None) -> None:
)
def reset_generated(task_id, delete_file=False, path=None, conn=None) -> dict:
"""Clear local AI result and move the task back to collected state."""
def reset_generated(
task_id,
reset_title=True,
reset_cover=True,
delete_file=False,
path=None,
conn=None,
) -> dict:
"""Clear selected local AI result components and keep the task generatable."""
if not reset_title and not reset_cover:
raise DbError("至少需要选择重置标题或重置封面")
if delete_file and not reset_cover:
raise DbError("只有重置封面时才允许删除本地新封面文件")
with _connection(conn, path) as database:
before = get_task(task_id, conn=database)
if before is None:
raise DbError(f"任务不存在: {task_id}")
new_cover_path = before.new_cover_path
deleted_file = None
if delete_file:
raise DbError("重置生成结果默认不删除本地新封面文件,请手动确认后清理")
if delete_file and reset_cover and new_cover_path and os.path.isfile(str(new_cover_path)):
os.remove(str(new_cover_path))
deleted_file = new_cover_path
now = _now()
new_title = None if reset_title else before.new_title
new_cover = None if reset_cover else before.new_cover_path
with database:
database.execute(
"""
UPDATE tasks
SET new_title = NULL,
new_cover_path = NULL,
stage = 'collected',
SET new_title = ?,
new_cover_path = ?,
stage = 'generated',
status = 'success',
last_error = NULL,
generated_at = NULL,
updated_at = ?
WHERE id = ?
""",
(now, int(task_id)),
(new_title, new_cover, now, int(task_id)),
)
after = get_task(task_id, conn=database)
return {
@@ -777,6 +790,8 @@ def reset_generated(task_id, delete_file=False, path=None, conn=None) -> dict:
"after": after,
"new_cover_path": new_cover_path,
"deleted_file": deleted_file,
"reset_title": bool(reset_title),
"reset_cover": bool(reset_cover),
}
+176 -37
View File
@@ -21,6 +21,11 @@ class GenerateTab(QWidget):
("略过", "skipped"),
("已更新", "applied"),
]
RESET_MODE_LABELS = {
"title": "标题",
"cover": "封面",
"all": "标题和封面",
}
def __init__(
self,
@@ -162,7 +167,7 @@ class GenerateTab(QWidget):
self.model = GenerateTaskTableModel(self.task_table, db_path=self.db_path, status_callback=self._set_status)
self.task_table.setModel(self.model)
self.task_table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.task_table.setSelectionMode(QAbstractItemView.SingleSelection)
self.task_table.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.task_table.setEditTriggers(QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed)
self.task_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.task_table.verticalHeader().setVisible(False)
@@ -531,57 +536,191 @@ class GenerateTab(QWidget):
if self.generate_thread is not None:
self._set_status("AI 生成正在进行,不能重置")
return
task = self._selected_task()
if task is None:
self._set_status("请选择要重置生成结果的任务")
scope, tasks = self._reset_scope_tasks()
if not tasks:
self._set_status("当前没有可重置生成结果的任务")
return
has_ai_result = bool(getattr(task, "new_title", None) or getattr(task, "new_cover_path", None))
if not has_ai_result and getattr(task, "stage", None) not in {"generated", "applied"}:
self._set_status("选中任务没有可重置的生成结果")
if not self._resettable_tasks(tasks, reset_title=True, reset_cover=True):
self._set_status("当前范围没有可重置的生成结果")
return
lines = [
"确定重置当前选中任务的本地生成结果吗?",
"",
f"商品ID:{task.item_id}",
f"店铺:{self.model.account_name_for(task)}",
"",
"将清空新标题、新封面路径和错误信息,并退回到已采集状态。",
"默认不删除本地新封面文件,不触碰 Shopee,也不会自动回写 Excel。",
]
if getattr(task, "new_cover_path", None):
lines.append(f"本地新封面文件保留:{task.new_cover_path}")
if getattr(task, "committed", 0):
lines.extend([
"",
"注意:该记录曾经提交过线上。本地重置不会回滚 Shopee,后续重新生成/更新可能再次提交线上。",
])
answer = QMessageBox.question(
self,
"重置生成结果",
"\n".join(lines),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
)
if answer != QMessageBox.Yes:
mode = self._ask_reset_generated_mode(tasks, scope)
if mode is None:
self._set_status("已取消重置生成结果")
return
reset_title, reset_cover = self._reset_mode_flags(mode)
target_tasks = self._resettable_tasks(
tasks,
reset_title=reset_title,
reset_cover=reset_cover,
)
if not target_tasks:
self._set_status(f"当前范围没有可重置的{self.RESET_MODE_LABELS[mode]}")
return
label = self.RESET_MODE_LABELS[mode]
try:
db.reset_generated(task.id, path=self.db_path)
message = (
"action=reset_generated step=db_write result=success "
f"detail=清空AI生成结果 task_id={task.id}"
for task in target_tasks:
db.reset_generated(
task.id,
reset_title=reset_title,
reset_cover=reset_cover,
path=self.db_path,
)
run_id = self._write_generated_reset_run_log(
target_tasks,
scope,
mode,
reset_title=reset_title,
reset_cover=reset_cover,
)
run_id = _write_reset_run_log(self.db_path, task, "reset_generated", message)
except Exception as exc:
QMessageBox.warning(self, "重置生成结果", str(exc))
self._set_status(f"重置生成结果失败:{exc}")
return
message = (
"action=reset_generated step=db_write result=success "
f"detail=重置{label} scope={scope} total={len(target_tasks)}"
)
self.refresh_tasks()
self._append_generate_log(message)
self._set_status(
f"已重置生成结果:商品 {task.item_id},run_id={run_id}"
f"已重置生成结果:{len(target_tasks)} 条,内容:{label},run_id={run_id}"
)
def _reset_scope_tasks(self):
selection = self.task_table.selectionModel()
selected_rows = []
if selection is not None:
selected_rows = sorted({index.row() for index in selection.selectedRows()})
if selected_rows:
tasks = [self.model.task_at(row) for row in selected_rows]
return "selected", [task for task in tasks if task is not None]
return "filtered", list(self.model.tasks)
def _ask_reset_generated_mode(self, tasks, scope):
scope_text = "选中任务" if scope == "selected" else "当前筛选结果"
title_count = sum(1 for task in tasks if getattr(task, "new_title", None))
cover_count = sum(1 for task in tasks if getattr(task, "new_cover_path", None))
committed_count = sum(1 for task in tasks if int(getattr(task, "committed", 0) or 0))
running_count = sum(1 for task in tasks if getattr(task, "status", None) == "running")
sample_ids = [str(getattr(task, "item_id", "") or "") for task in tasks[:5]]
sample_ids = [item_id for item_id in sample_ids if item_id]
sample_text = "、".join(sample_ids)
if len(tasks) > 5:
sample_text += f" 等 {len(tasks)} 条"
lines = [
"请选择要重置的本地生成内容。",
"",
f"作用范围:{scope_text} {len(tasks)} 条",
f"包含新标题:{title_count} 条",
f"包含新封面:{cover_count} 条",
]
if sample_text:
lines.append(f"商品ID:{sample_text}")
if running_count:
lines.append(f"运行中任务会跳过:{running_count} 条")
lines.extend([
"",
"重置标题:只清空新标题,保留新封面。",
"重置封面:只清空新封面,保留新标题和手动微调标题。",
"重置全部:清空新标题和新封面。",
"",
"不会触碰 Shopee,也不会自动回写 Excel;后续重新生成会消耗 cmhub 点数。",
"默认不删除本地新封面文件。",
])
if committed_count:
lines.extend([
"",
f"注意:其中 {committed_count} 条已经提交过线上。本地重置不会回滚 Shopee,重生成后再更新会再次提交线上。",
])
box = QMessageBox(self)
box.setWindowTitle("重置生成结果")
box.setText("\n".join(lines))
title_button = box.addButton("重置标题", QMessageBox.AcceptRole)
cover_button = box.addButton("重置封面", QMessageBox.AcceptRole)
all_button = box.addButton("重置全部", QMessageBox.DestructiveRole)
cancel_button = box.addButton("取消", QMessageBox.RejectRole)
box.setDefaultButton(cancel_button)
box.exec()
clicked = box.clickedButton()
if clicked is title_button:
return "title"
if clicked is cover_button:
return "cover"
if clicked is all_button:
return "all"
return None
def _reset_mode_flags(self, mode):
if mode == "title":
return True, False
if mode == "cover":
return False, True
return True, True
def _resettable_tasks(self, tasks, reset_title=True, reset_cover=True):
resettable = []
for task in tasks:
if getattr(task, "status", None) == "running":
continue
has_title = bool(getattr(task, "new_title", None))
has_cover = bool(getattr(task, "new_cover_path", None))
if reset_title and has_title:
resettable.append(task)
continue
if reset_cover and has_cover:
resettable.append(task)
return resettable
def _write_generated_reset_run_log(
self,
tasks,
scope,
mode,
reset_title=True,
reset_cover=True,
):
task_ids = [getattr(task, "id", None) for task in tasks]
committed_count = sum(1 for task in tasks if int(getattr(task, "committed", 0) or 0))
options = {
"action": "reset_generated",
"scope": scope,
"mode": mode,
"reset_title": bool(reset_title),
"reset_cover": bool(reset_cover),
"task_ids": task_ids,
"committed_count": committed_count,
}
run_id = db.create_run_log(
"reset",
total=len(tasks),
options=options,
path=self.db_path,
)
label = self.RESET_MODE_LABELS[mode]
for task in tasks:
message = (
"action=reset_generated step=db_write result=success "
f"detail=重置{label} task_id={getattr(task, 'id', None)}"
)
db.add_run_log_event(
run_id,
message,
task_id=getattr(task, "id", None),
alias=getattr(task, "alias", None),
item_id=getattr(task, "item_id", None),
path=self.db_path,
)
db.finish_run_log(
run_id,
status="done",
done=len(tasks),
success_count=len(tasks),
failed_count=0,
summary_json=options,
path=self.db_path,
)
return run_id
def show_task_images(self, index):
if index.isValid() and index.column() == 3:
return