feat: add selected task reset actions

This commit is contained in:
chengma
2026-06-30 11:50:49 +08:00
parent 580a7a570d
commit f7ea9ec0fa
6 changed files with 412 additions and 8 deletions
+75
View File
@@ -518,6 +518,16 @@ def list_tasks(batch_id=None, stage=None, status=None, alias=None, path=None, co
return _fetch_all(database, sql, params, Task)
def get_task(task_id, path=None, conn=None):
with _connection(conn, path) as database:
return _fetch_one(
database,
"SELECT * FROM tasks WHERE id = ?",
(int(task_id),),
Task,
)
def mark_running(task_id, phase, path=None, conn=None) -> None:
_attempt_field(phase)
with _connection(conn, path) as database:
@@ -639,6 +649,71 @@ 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."""
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("重置生成结果默认不删除本地新封面文件,请手动确认后清理")
now = _now()
with database:
database.execute(
"""
UPDATE tasks
SET new_title = NULL,
new_cover_path = NULL,
stage = 'collected',
status = 'success',
last_error = NULL,
generated_at = NULL,
updated_at = ?
WHERE id = ?
""",
(now, int(task_id)),
)
after = get_task(task_id, conn=database)
return {
"task_id": int(task_id),
"before": before,
"after": after,
"new_cover_path": new_cover_path,
"deleted_file": deleted_file,
}
def reset_apply_status(task_id, path=None, conn=None) -> dict:
"""Move a generated/applied local task back to pending update state."""
with _connection(conn, path) as database:
before = get_task(task_id, conn=database)
if before is None:
raise DbError(f"任务不存在: {task_id}")
now = _now()
with database:
database.execute(
"""
UPDATE tasks
SET stage = 'generated',
status = 'pending',
last_error = NULL,
updated_at = ?
WHERE id = ?
""",
(now, int(task_id)),
)
after = get_task(task_id, conn=database)
return {
"task_id": int(task_id),
"before": before,
"after": after,
}
def create_run_log(
run_type,
dry_run=False,
+163
View File
@@ -104,6 +104,43 @@ if QT_IMPORT_ERROR is None:
return db_path or appconfig.db_path(config)
def _write_reset_run_log(db_path, task, action, message):
run_id = db.create_run_log(
"reset",
total=1,
options={
"action": action,
"task_id": getattr(task, "id", None),
"alias": getattr(task, "alias", None),
"item_id": getattr(task, "item_id", None),
},
path=db_path,
)
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=db_path,
)
db.finish_run_log(
run_id,
status="done",
done=1,
success_count=1,
failed_count=0,
summary_json={
"action": action,
"task_id": getattr(task, "id", None),
"alias": getattr(task, "alias", None),
"item_id": getattr(task, "item_id", None),
},
path=db_path,
)
return run_id
class TaskTableModel(QAbstractTableModel):
"""Table model for task rows shared by workflow tabs."""
@@ -450,6 +487,8 @@ if QT_IMPORT_ERROR is None:
self.preview_prompt_button = QPushButton("预览")
self.generate_button = QPushButton("开始生成")
self.stop_generate_button = QPushButton("停止")
self.reset_generate_button = QPushButton("重置生成结果")
self.reset_generate_button.setObjectName("resetGenerateButton")
self.stop_generate_button.setEnabled(False)
self.progress_label = QLabel("进度:标题0/0 · 封面0/0 · 失败0")
@@ -535,6 +574,7 @@ if QT_IMPORT_ERROR is None:
bottom_layout.addStretch(1)
bottom_layout.addWidget(self.generate_button)
bottom_layout.addWidget(self.stop_generate_button)
bottom_layout.addWidget(self.reset_generate_button)
layout = QVBoxLayout(self)
layout.setContentsMargins(18, 18, 18, 18)
@@ -557,6 +597,7 @@ if QT_IMPORT_ERROR is None:
self.preview_prompt_button.clicked.connect(self.preview_cover_prompt)
self.generate_button.clicked.connect(self.start_generate)
self.stop_generate_button.clicked.connect(self.stop_generate)
self.reset_generate_button.clicked.connect(self.reset_generated_result)
self.task_table.doubleClicked.connect(self.show_task_images)
self.refresh_cover_templates()
@@ -773,6 +814,61 @@ if QT_IMPORT_ERROR is None:
self.generate_worker.cancel()
self._set_status("正在停止 AI 生成...")
def reset_generated_result(self, checked=False):
if self.generate_thread is not None:
self._set_status("AI 生成正在进行,不能重置")
return
task = self._selected_task()
if task is None:
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("选中任务没有可重置的生成结果")
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:
self._set_status("已取消重置生成结果")
return
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}"
)
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
self.refresh_tasks()
self._append_generate_log(message)
self._set_status(
f"已重置生成结果:商品 {task.item_id},run_id={run_id}"
)
def show_task_images(self, index):
task = self.model.task_at(index.row()) if index.isValid() else self._selected_task()
if task is None:
@@ -820,6 +916,7 @@ if QT_IMPORT_ERROR is None:
def _set_generate_running(self, running):
self.generate_button.setEnabled(not running)
self.stop_generate_button.setEnabled(running)
self.reset_generate_button.setEnabled(not running)
self.refresh_button.setEnabled(not running)
self.batch_filter.setEnabled(not running)
self.shop_filter.setEnabled(not running)
@@ -1077,6 +1174,8 @@ if QT_IMPORT_ERROR is None:
self.start_update_button = QPushButton("开始更新")
self.stop_update_button = QPushButton("停止")
self.reset_update_button = QPushButton("重置更新状态")
self.reset_update_button.setObjectName("resetUpdateButton")
self.write_back_button = QPushButton("回写结果到 Excel")
self.stop_update_button.setEnabled(False)
self.write_back_button.setEnabled(False)
@@ -1084,6 +1183,7 @@ if QT_IMPORT_ERROR is None:
action_layout = QHBoxLayout()
action_layout.addWidget(self.start_update_button)
action_layout.addWidget(self.stop_update_button)
action_layout.addWidget(self.reset_update_button)
action_layout.addStretch(1)
action_layout.addWidget(self.write_back_button)
@@ -1104,6 +1204,7 @@ if QT_IMPORT_ERROR is None:
self.refresh_button.clicked.connect(self.refresh_tasks)
self.start_update_button.clicked.connect(self.start_update)
self.stop_update_button.clicked.connect(self.stop_update)
self.reset_update_button.clicked.connect(self.reset_apply_status)
self.write_back_button.clicked.connect(self.write_back_results)
self.refresh_tasks()
@@ -1217,6 +1318,66 @@ if QT_IMPORT_ERROR is None:
return
self._start_result_write_back(batch_ids, auto=False)
def reset_apply_status(self, checked=False):
if self.apply_thread is not None or self.result_write_back_thread is not None:
self._set_status("更新或回写正在进行,不能重置")
return
task = self._selected_task()
if task is None:
self._set_status("请选择要重置更新状态的任务")
return
if not (getattr(task, "new_title", None) or getattr(task, "new_cover_path", None)):
self._set_status("选中任务没有新标题或新封面,不能重置为可更新")
return
lines = [
"确定重置当前选中任务的本地更新状态吗?",
"",
f"商品ID:{task.item_id}",
f"店铺:{self.model.account_name_for(task)}",
"",
"将保留新标题和新封面路径,只把本地状态退回可更新。",
"不会触碰 Shopee,也不会自动回写 Excel。",
]
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:
self._set_status("已取消重置更新状态")
return
try:
db.reset_apply_status(task.id, path=self.db_path)
message = (
"action=reset_apply_status step=db_write result=success "
f"detail=退回可更新 task_id={task.id}"
)
run_id = _write_reset_run_log(self.db_path, task, "reset_apply_status", message)
except Exception as exc:
QMessageBox.warning(self, "重置更新状态", str(exc))
self._set_status(f"重置更新状态失败:{exc}")
return
self.refresh_tasks()
self._append_run_log(message)
self._set_status(
f"已重置更新状态:商品 {task.item_id},run_id={run_id}"
)
def _selected_task(self):
index = self.task_table.currentIndex()
if index.isValid():
return self.model.task_at(index.row())
if self.model.rowCount() > 0:
return self.model.task_at(0)
return None
def _is_actionable_task(self, task):
return (
getattr(task, "stage", None) == "generated"
@@ -1378,6 +1539,7 @@ if QT_IMPORT_ERROR is None:
def _set_apply_running(self, running):
self.start_update_button.setEnabled(not running)
self.stop_update_button.setEnabled(running)
self.reset_update_button.setEnabled(not running)
self.refresh_button.setEnabled(not running)
self.batch_filter.setEnabled(not running)
self.shop_filter.setEnabled(not running)
@@ -1387,6 +1549,7 @@ if QT_IMPORT_ERROR is None:
def _set_result_write_back_running(self, running):
self.start_update_button.setEnabled(not running)
self.reset_update_button.setEnabled(not running)
self.refresh_button.setEnabled(not running)
self.batch_filter.setEnabled(not running)
self.shop_filter.setEnabled(not running)
+11 -5
View File
@@ -88,6 +88,9 @@ mark_skipped(task_id, reason) -> None # status=skipped,stage 不前
set_collected(task_id, old_title, old_cover_path) -> None # stage=collected,status=success,collect_attempts+1
set_generated(task_id, new_title, new_cover_path) -> None # stage=generated,status=success,generate_attempts+1
set_applied(task_id, committed, error=None) -> None # 成功 stage=applied;失败 status=failed 且 stage 不前进
# T-404a 已实现:选中记录重置
reset_generated(task_id, delete_file=False) -> dict # 本地清空 AI 结果并退回 collected;默认不删新封面文件
reset_apply_status(task_id) -> dict # 本地退回 generated/pending 供重复更新;保留 new_* 与 committed 历史事实
# 运行日志
create_run_log(run_type, dry_run=False, total=0, options=None) -> int
finish_run_log(run_id, **fields) -> None # status/done/success_count/skipped_count/failed_count/summary_json
@@ -232,11 +235,11 @@ collect(account, task) -> dict # -> {old_title, old_cover_path}
# 应用
change_title(cdp, new_title) -> dict # {ok, value, modelvalue},要求三者相等
replace_cover(cdp, image_win_path, old_cover_path=None) -> dict
# 上传→等CDN→拖第一位;满9张时先确认 old_cover_path 非空且文件存在,
# 上传→等 Shopee CDN 地址→拖第一位;失败返回 upload_state 诊断;满9张时先确认 old_cover_path 非空且文件存在,
# 再点第一张删除和可见确认框;备份缺失时返回 OLD_COVER_BACKUP_MISSING,不删除线上图片
click_update(cdp) -> dict # {clicked, reason};禁用则记失败
click_update(cdp, confirm_timeout=3) -> dict # {clicked, reason, toasts, confirm?};禁用或 Shopee 二次确认未完成则记失败
apply_task(account, task, close_success_tab=False) -> dict
# 对已生成任务:换标题+换封面+点「更新」提交(调用前必须已通过安全开关和批量确认)
# 对已生成任务:换标题+换封面+点页面「更新」;若出现 Shopee 确认框,只点弹窗主按钮「更新」,不点「立即優化」(调用前必须已通过安全开关和批量确认)
# close_success_tab=True 时,仅成功提交且商品页为本轮自动新开时关闭该 tab
# -> {committed, error}
```
@@ -248,7 +251,8 @@ apply_task(account, task, close_success_tab=False) -> dict
- `CDP.close()` 只断开当前 websocket 控制连接,不关闭 Chrome 页面。
- `open_product()` 若复用已存在商品 tab,则标记为用户已有页面;若调用 `create_tab()` 新建,则记录 target id。
- `collect()` 结束时只关闭本轮自动新建的商品编辑页 tab;用户原本打开的商品 tab 不关闭。
- ③ 更新流程默认不关闭商品页;若 `close_success_tab=True`,只在提交成功且商品页为本轮自动新开时关闭,失败任务和用户原本打开的 tab 保留现场。
- ③ 更新流程默认不关闭商品页;若 `close_success_tab=True`,只在提交成功且商品页为本轮自动新开时关闭,失败任务和用户原本打开的 tab 保留现场。Shopee 确认成功后可能把当前 tab 跳回 `/portal/product/list/all?operationSortBy=modified_time`,`click_update()` 会把该 URL 记录到 `post_update.url` 并标记 `redirected_to_list=true`。
- `click_update()` 的提交成功定义:页面主「更新」按钮已点击,且 Shopee 站点侧确认框未出现或已在可见 `.eds-modal__content` / `.eds-modal__box` 内点击主按钮「更新」。如果确认框仍停留、只点到页面主按钮、或误入「立即優化」,必须返回失败并保留现场。
- T-502 满 9 张封面删除前,`apply_task()` 应把任务的 `old_cover_path` 传给 `replace_cover()`;`replace_cover()` 只有在本地旧封面备份存在时才允许进入删第一张流程。
## ai 模块(`app/ai.py`,已建,外部 AI,通用 HTTP)
@@ -384,6 +388,7 @@ TAB_STYLE: str # 顶层 Tab 栏防误点样式:
- 底部「开始生成」只处理当前筛选结果里 `stage=collected` 的任务;批次/店铺/商品ID/状态筛选共同决定当前筛选结果;通过 `GenerateWorker` 调 `ai.generate_batch()`,先并发标题、再并发封面。
- 「停止」调用 worker 的协作式 `cancel()`;未开始的 Future 取消,不记失败;已完成的 `db.set_generated()` 结果保留,可再次开始生成剩余任务。
- 进度标签显示:`标题x/n · 封面x/n · 失败z`;每条封面生成成功后立即 `db.set_generated()`,失败 `db.mark_failed(..., "generate", error)`,GUI 刷新任务表。
- T-404a 已实现:「重置生成结果」读取当前选中单条;确认后调用 DB reset,清空本地 AI 结果并退回可生成状态;默认不删除本地新封面文件;写 `run_type=reset` 事件。
③ 更新shopee当前要点(T-303b/T-401/T-402/T-403):
@@ -394,13 +399,14 @@ TAB_STYLE: str # 顶层 Tab 栏防误点样式:
- 「开始更新」只读取当前筛选结果;无任务时只提示,不弹确认、不改库。
- 点击「开始更新」先读取 `shopee_update`:dry-run 关闭且未允许真实提交、超过单次最大条数、包含非测试商品 ID、或包含新封面但未允许封面更新时,弹警告并阻断;通过后才弹窗展示批次/店铺/商品ID/状态/任务数、提交线上风险和当前安全设置。
- 用户点否/取消时不执行、不改库;用户点是后才创建 `ApplyWorker`。dry-run 开启时只预览;dry-run 关闭时才可能真实提交。
- `ApplyWorker` 只处理当前筛选结果里 `stage=generated` 且已有新标题或新封面、状态为 `success/pending/failed` 的任务;已更新和略过记录仅查看,不会再次提交。
- `ApplyWorker` 只处理当前筛选结果里 `stage=generated` 且已有新标题或新封面、状态为 `success/pending/failed` 的任务;已更新和略过记录仅查看,不会再次提交,除非用户先用 T-404a 的「重置更新状态」把选中记录退回可更新。
- dry-run:不做账号登录预检,不调用 `editor.apply_task()`,不写任务状态,不回写 Excel;只把每条“将更新/将略过”写入运行日志并弹汇总。
- 真实更新前先做账号就绪预检:无账号、当前筛选结果匹配账号 Chrome 未启动、未登录,或本轮涉及账号调试端口冲突时,返回 `blocked=True`,GUI 弹窗汇总并跳转/引导去④账号管理;预检不通过时不调用 `editor.apply_task()`、不写失败状态。
- 预检通过后默认串行;若 `parallel_accounts=true` 且 `max_parallel_accounts>1`,按账号分组并行执行,不同账号可同时跑,同一账号内仍串行。每条执行 `db.mark_running(..., "apply")` → `editor.apply_task(account, task, close_success_tab=设置值)` → `db.set_applied()`;成功推进 `stage=applied/status=success/committed=1`,失败保持原 stage、`status=failed/committed=0/last_error`,单条失败继续下一条。
- dry-run 和真实更新都会创建 `run_logs`,并把逐条事件写入 `run_log_events`;③ 页面显示最近运行日志。
- 若 `close_success_tab=true`,`editor.apply_task()` 只关闭本轮自动新开且成功提交的商品页;失败和复用的用户已有 tab 不关闭。
- 别名未匹配账号的任务逐条 `db.mark_skipped()`,原因 `别名未匹配账号`;「停止」调用 worker 协作式 `cancel()`,已开始单条跑到安全边界后结束。
- T-404a 已实现:「重置更新状态」读取当前选中单条;确认后保留 `new_title/new_cover_path`,本地退回 `stage=generated/status=pending` 供重复更新;`committed=1` 时必须提示线上已提交过且不回滚 Shopee,并保留 committed 历史事实/运行日志。
- ③ 没有常驻提交开关;确认弹窗是提交线上前的边界。
- 更新完成后自动调用 `WriteBackWorker(mode="results")` → `excel.write_back_results()`,把新标题、新封面路径、更新状态写回原 Excel;原文件被占用时提示关闭后点击「回写结果到 Excel」手动重试,SQLite 更新结果不回滚。
- 自动结果回写完成后弹窗汇总成功/失败/略过数量与 Excel 回写文件/行数;若没有可回写批次,也会弹出更新汇总。
+5 -3
View File
@@ -72,7 +72,7 @@
│ │[ ]│ │ (双击某行 → 弹窗看 旧封面 | 新封面) │ │
│ │ │ │ 运行日志:逐条记录标题/封面生成步骤与失败原因 │ │
│ └──────────┘ └───────────────────────────────────────────────┘ │
│ 进度:标题30/30 · 封面12/30 · 失败1 [▶ 开始生成] [■停止] │
│ 进度:标题30/30 · 封面12/30 · 失败1 [▶ 开始生成][■停止][重置生成结果] │
└───────────────────────────────────────────────────────────────┘
```
@@ -85,6 +85,7 @@
- 底部**单个「开始生成」+「停止」**:开始生成 = **先按 `title_concurrency` 并发生成标题,接着按 `image_concurrency` 并发生成封面**;进度实时显示 标题/封面/失败 计数。
- 生成参数(标题/图片并发数、失败重试、分辨率、jpg 质量、模型/Key)在 **⑤ 设置**,不在本 Tab 重复。
- 每条/每张完成即写库;`run_type=generate` 写 `run_logs/run_log_events`,图片生成失败同时写本地 `logs/cmshopee.log` 脱敏 traceback;「停止」取消未开始项,可再次「开始生成」对剩余继续。
- 「重置生成结果」仅作用当前选中单条,运行中禁用;确认后只改本地 DB,清空 `new_title/new_cover_path/last_error` 并退回 `collected/success` 供重新生成,默认不删除本地新封面文件。若该记录已提交过线上,必须在确认框提示本地重置不回滚 Shopee。
- **无逐条人工审核环节**;新标题直接用 AI 输出(不可编辑);可选对单行 `重生成`。生成完即可进入 ③,③ 开始更新前会做批量确认。
## ③ 更新shopee
@@ -97,7 +98,7 @@
│ │ 店铺 商品ID 新标题 新封面 阶段 结果 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ 运行日志:dry-run/真实更新逐条留痕 │
│ [▶ 开始更新] [■停止] [回写结果到 Excel] │
│ [▶ 开始更新] [■停止] [重置更新状态] [回写结果到 Excel] │
└───────────────────────────────────────────────────────────────┘
```
@@ -106,13 +107,14 @@
- 状态筛选:`已生成` 只跑未更新的;`失败` 用于**失败重试**;`已更新成功/略过` 仅查看。
- 点击「开始更新」先读取 ⑤ `shopee_update` 安全设置:dry-run 关闭且未允许真实提交、超过单次最大条数、包含非测试商品 ID、或包含新封面但未允许封面更新时,直接弹警告并阻断;dry-run 开启时只预览,不进入 Shopee。
- 安全开关通过后,弹窗展示本次筛选条件、任务数量、安全设置和“将提交线上”的风险提示;用户点「是/确认」才开始,点「否/取消」不执行。
- 对确认后的**已生成(generated)任务**执行:打开编辑页换标题+换封面 → 点「更新」提交。
- 对确认后的**已生成(generated)任务**执行:打开编辑页换标题+换封面 → 点页面「更新」 → 如 Shopee 弹出“確定您要更新商品嗎?”确认框(`.eds-modal__content` / `.eds-modal__box`),则只点弹窗主按钮「更新」提交,不点「立即優化」。
- 满 9 张封面时,删除第一张前必须已有该任务的本地旧封面备份(①采集得到的 `old_cover_path` 且文件存在);备份缺失时阻断该条更新并提示先采集/修复备份,不盲删线上图片。
- 默认串行、单条失败继续;⑤ 可开启多账号并行,不同账号同时执行,同一账号内仍串行;真实更新前若本轮账号调试端口冲突则阻断。
- dry-run 只写运行日志与预览汇总,不打开 Shopee、不调用 `editor.apply_task()`、不写任务状态、不回写 Excel。
- 真实更新每条立即写回 SQLite(committed/状态/error),失败不阻塞后续任务;dry-run 和真实更新都会写 `run_logs/run_log_events`。
- 更新前做账号就绪预检:无账号、对应账号 Chrome 未启动或未登录时整体阻断并引导去④账号管理,不进入逐条提交。
- 若 ⑤ 开启“成功后关闭本次新开编辑页”,则仅关闭本轮程序自动新开且成功提交的商品页;失败任务和用户原本打开的 tab 不关闭。
- 「重置更新状态」仅作用当前选中单条,运行中禁用;保留 `new_title/new_cover_path`,只把本地状态退回可更新,用于重复测试上传/提交。若 `committed=1`,确认框必须提示线上已提交过、本地重置不回滚 Shopee、重复更新会再次提交;不得静默清除 committed 历史事实。
- 更新完成后自动回写原 Excel:写入新标题、新封面图片路径、更新状态;原文件被锁时提示关闭后点击「回写结果到 Excel」手动重试。
- 自动回写完成后弹窗汇总成功/失败/略过数量与 Excel 回写文件/行数。
+52
View File
@@ -115,6 +115,58 @@ class DbTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_reset_generated_and_apply_status_keep_local_history(self):
with self.make_temp_dir() as temp_dir:
db_path = os.path.join(temp_dir, "cmshopee.db")
db.init_db(db_path)
batch_id = db.create_batch(["input.xlsx"], path=db_path)
db.insert_tasks(
batch_id,
[
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "Sheet1",
"source_row": 2,
"account_name": "shop",
"alias": "alias",
"item_id": "51100639510",
}
],
path=db_path,
)
task = db.list_tasks(batch_id=batch_id, path=db_path)[0]
new_cover = os.path.join(temp_dir, "new.jpg")
with open(new_cover, "wb") as fh:
fh.write(b"jpeg")
db.set_collected(task.id, "旧标题", "old.jpg", path=db_path)
db.set_generated(task.id, "新标题", new_cover, path=db_path)
db.set_applied(task.id, True, path=db_path)
reset_apply = db.reset_apply_status(task.id, path=db_path)
self.assertEqual("applied", reset_apply["before"].stage)
after_apply = reset_apply["after"]
self.assertEqual("generated", after_apply.stage)
self.assertEqual("pending", after_apply.status)
self.assertEqual("新标题", after_apply.new_title)
self.assertEqual(new_cover, after_apply.new_cover_path)
self.assertEqual(1, after_apply.committed)
reset_generated = db.reset_generated(task.id, path=db_path)
after_generated = reset_generated["after"]
self.assertEqual(new_cover, reset_generated["new_cover_path"])
self.assertIsNone(reset_generated["deleted_file"])
self.assertTrue(os.path.exists(new_cover))
self.assertEqual("collected", after_generated.stage)
self.assertEqual("success", after_generated.status)
self.assertIsNone(after_generated.new_title)
self.assertIsNone(after_generated.new_cover_path)
self.assertIsNone(after_generated.last_error)
self.assertEqual(1, after_generated.committed)
self.assert_removed(temp_dir)
def test_duplicate_task_and_invalid_update_raise_clear_errors(self):
with self.make_temp_dir() as temp_dir:
db_path = os.path.join(temp_dir, "cmshopee.db")
+106
View File
@@ -509,6 +509,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual("保存标题提示词", tab.save_title_button.text())
self.assertEqual("开始生成", tab.generate_button.text())
self.assertEqual("停止", tab.stop_generate_button.text())
self.assertEqual("重置生成结果", tab.reset_generate_button.text())
self.assertFalse(tab.stop_generate_button.isEnabled())
self.assertEqual("进度:标题0/0 · 封面0/0 · 失败0", tab.progress_label.text())
self.assertEqual("默认", tab.cover_template_combo.currentText())
@@ -595,6 +596,58 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_generate_tab_resets_selected_generated_result_and_writes_reset_log(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
db.insert_tasks(
batch_id,
[
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "商品",
"source_row": 2,
"account_name": "Excel主店",
"alias": "alias-a",
"item_id": "51100639510",
}
],
path=cfg["db_path"],
)
task = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])[0]
new_cover = os.path.join(temp_dir, "new.jpg")
with open(new_cover, "wb") as fh:
fh.write(b"jpeg")
db.set_collected(task.id, "旧标题", "old.jpg", path=cfg["db_path"])
db.set_generated(task.id, "新标题", new_cover, path=cfg["db_path"])
statuses = []
tab = GenerateTab(config=cfg, status_callback=statuses.append)
self.addCleanup(tab.close)
tab.task_table.selectRow(0)
with mock.patch("app.gui.QMessageBox.question", return_value=gui.QMessageBox.Yes) as question:
tab.reset_generated_result()
message = question.call_args[0][2]
self.assertIn("默认不删除本地新封面文件", message)
self.assertIn(new_cover, message)
updated = db.get_task(task.id, path=cfg["db_path"])
self.assertEqual("collected", updated.stage)
self.assertEqual("success", updated.status)
self.assertIsNone(updated.new_title)
self.assertIsNone(updated.new_cover_path)
self.assertTrue(os.path.exists(new_cover))
run_log = db.list_run_logs(limit=1, run_type="reset", path=cfg["db_path"])[0]
self.assertEqual("done", run_log.status)
self.assertEqual("reset_generated", run_log.options["action"])
events = db.list_run_log_events(run_log.id, path=cfg["db_path"])
self.assertTrue(any("action=reset_generated" in event.message for event in events))
self.assertIn("已重置生成结果", statuses[-1])
self.assert_removed(temp_dir)
def test_generate_worker_calls_generate_batch_and_emits_signals(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
@@ -847,6 +900,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual("开始更新", tab.start_update_button.text())
self.assertEqual("applyItemFilter", tab.item_filter.objectName())
self.assertEqual("停止", tab.stop_update_button.text())
self.assertEqual("重置更新状态", tab.reset_update_button.text())
self.assertFalse(tab.stop_update_button.isEnabled())
self.assertEqual(2, tab.model.rowCount())
self.assertEqual("主店", tab.model.index(0, 0).data())
@@ -890,6 +944,58 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_apply_tab_resets_selected_apply_status_and_warns_committed_task(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
db.insert_tasks(
batch_id,
[
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "商品",
"source_row": 2,
"account_name": "Excel主店",
"alias": "alias-a",
"item_id": "51100639510",
}
],
path=cfg["db_path"],
)
task = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])[0]
db.set_collected(task.id, "旧标题", "old.jpg", path=cfg["db_path"])
db.set_generated(task.id, "新标题", "new.jpg", path=cfg["db_path"])
db.set_applied(task.id, True, path=cfg["db_path"])
statuses = []
tab = ApplyTab(config=cfg, status_callback=statuses.append)
self.addCleanup(tab.close)
tab.status_filter.setCurrentIndex(tab.status_filter.findData("applied"))
tab.task_table.selectRow(0)
with mock.patch("app.gui.QMessageBox.question", return_value=gui.QMessageBox.Yes) as question:
tab.reset_apply_status()
message = question.call_args[0][2]
self.assertIn("已经提交过线上", message)
self.assertIn("本地重置不会回滚 Shopee", message)
self.assertIn("重复更新会再次提交线上", message)
updated = db.get_task(task.id, path=cfg["db_path"])
self.assertEqual("generated", updated.stage)
self.assertEqual("pending", updated.status)
self.assertEqual("新标题", updated.new_title)
self.assertEqual("new.jpg", updated.new_cover_path)
self.assertEqual(1, updated.committed)
run_log = db.list_run_logs(limit=1, run_type="reset", path=cfg["db_path"])[0]
self.assertEqual("done", run_log.status)
self.assertEqual("reset_apply_status", run_log.options["action"])
events = db.list_run_log_events(run_log.id, path=cfg["db_path"])
self.assertTrue(any("action=reset_apply_status" in event.message for event in events))
self.assertIn("已重置更新状态", statuses[-1])
self.assert_removed(temp_dir)
def test_apply_tab_start_update_requires_confirmation_before_starting_worker(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)