fix: reset visible run logs for new runs

This commit is contained in:
chengma
2026-07-02 17:32:51 +08:00
parent 6e2dcf9954
commit f3e0956cf6
9 changed files with 149 additions and 18 deletions
+14 -2
View File
@@ -146,7 +146,7 @@ class ApplyTab(QWidget):
self.write_back_button.clicked.connect(self.write_back_results)
self.refresh_tasks()
self._load_latest_run_log()
self._show_current_run_log_empty()
def _set_status(self, message):
if self.status_callback is not None:
@@ -279,7 +279,7 @@ class ApplyTab(QWidget):
self.apply_worker = worker
self.apply_thread = thread
self._set_apply_running(True)
self.run_log_view.clear()
self._reset_run_log(tasks, dry_run=dry_run, batch_size=batch_size)
if dry_run:
self._set_status(f"开始检查本轮更新:{len(tasks)} 条")
else:
@@ -626,6 +626,18 @@ class ApplyTab(QWidget):
def _append_run_log(self, message):
self.run_log_view.appendPlainText(str(message))
scroll_bar = self.run_log_view.verticalScrollBar()
scroll_bar.setValue(scroll_bar.maximum())
def _show_current_run_log_empty(self):
self.run_log_view.setPlainText("本轮日志会在开始运行后显示")
def _reset_run_log(self, tasks, dry_run=False, batch_size=1):
self.run_log_view.clear()
action = "检查" if dry_run else "更新"
self._append_run_log(
f"本轮{action}开始:任务 {len(tasks)} 条,每批 {batch_size} 条"
)
def _load_latest_run_log(self):
try:
+12 -4
View File
@@ -238,7 +238,7 @@ class GenerateTab(QWidget):
self.refresh_cover_templates()
self.refresh_tasks()
self._load_latest_generate_run_log()
self._show_current_run_log_empty()
def _set_status(self, message):
if self.status_callback is not None:
@@ -253,6 +253,16 @@ class GenerateTab(QWidget):
scroll_bar = self.run_log_view.verticalScrollBar()
scroll_bar.setValue(scroll_bar.maximum())
def _show_current_run_log_empty(self):
self.run_log_view.setPlainText("本轮日志会在开始运行后显示")
def _reset_generate_run_log(self, tasks, generate_cover):
self.run_log_view.clear()
cover_text = "是" if generate_cover else "否"
self._append_generate_log(
f"本轮AI生成开始:任务 {len(tasks)} 条,生成封面:{cover_text}"
)
def _on_generate_cover_toggled(self, checked):
previous = bool(appconfig.ai_config(self.config).get("generate_cover", False))
if self._save_generate_cover_setting(show_status=True):
@@ -478,7 +488,7 @@ class GenerateTab(QWidget):
worker.failed.connect(self._on_generate_failed)
worker.finished.connect(self._on_generate_finished)
worker.cancelled.connect(self._on_generate_cancelled)
self.run_log_view.clear()
self._reset_generate_run_log(tasks, generate_cover)
thread = run_worker(worker, thread_name="GenerateWorker", start=False)
thread.finished.connect(lambda: self._forget_generate_thread(thread))
self.generate_worker = worker
@@ -641,7 +651,6 @@ class GenerateTab(QWidget):
def _on_generate_finished(self, payload):
self._set_generate_running(False)
self.refresh_tasks()
self._load_latest_generate_run_log()
self._update_generate_progress(payload)
if payload.get("error"):
self._set_status(f"AI 生成失败:{payload.get('error')}")
@@ -651,7 +660,6 @@ class GenerateTab(QWidget):
def _on_generate_cancelled(self, payload):
self._set_generate_running(False)
self.refresh_tasks()
self._load_latest_generate_run_log()
self._update_generate_progress(payload)
self._set_status("AI 生成已停止:" + self._generate_progress_text(payload))
+2 -2
View File
@@ -42,11 +42,11 @@
| 导入(①) | 导入多个 Excel,按模板解析输入列(账号名/别名/商品id)为任务列表 | P0 |
| 导入校验汇总(①) | 导入后展示文件数、解析行数(原始数据量)、有效/无效行、匹配账号行数(按账号细分)、未匹配行数;跑采集前先纠错 | P0 |
| 采集旧数据(①) | 程序只读打开商品页,抓取旧标题、下载旧封面到本地,回写 Excel 旧字段 | P0 |
| AI 生成(②) | 输入标题/封面提示词,AI 据提示词+旧标题生成新标题;封面图片按②本轮「生成封面图片(成本较高)」开关可选生成,避免用户无意产生图片模型成本 | P0 |
| AI 生成(②) | 输入标题/封面提示词,AI 据提示词+旧标题生成新标题;封面图片按②本轮「生成封面图片(成本较高)」开关可选生成,避免用户无意产生图片模型成本;点击「开始生成」时清空②界面旧日志,只显示本轮生成日志,历史 run_logs 仍保留 | P0 |
| 提示词管理(②) | 标题提示词「保存」到 `title_prompt.txt` 并启动回显;封面提示词多模板(下拉 + 新建/保存/另存为/重命名/删除)+ 插入 `{新标题}` + 预览(变量替换) | P0 |
| 查看对照(②) | 双击任务弹窗查看新旧封面(纯查看,无逐条审核阶段);可选对单行重生成 | P0 |
| 更新 shopee(③) | 按批次/店铺/状态筛选;可先「检查本轮更新」确认范围;点击「开始更新」后弹窗确认,确认后对当前筛选出的已生成任务按每批最大条数分批更新 Shopee,逐条换标题+封面并点「更新」提交线上;普通正式更新不再受测试商品 ID 限制,可批量更新真实商品;可按状态=失败重试 | P0 |
| 检查本轮更新 / 运行日志 / 多账号并行(③/⑤) | ③ 提供「检查本轮更新」按钮,不打开 Shopee、不提交、不改任务状态,只显示当前筛选范围、店铺分布、预计分批和会更新字段;真实更新写运行日志;可在⑤开启多账号并行,同账号内仍串行 | V2 已接入,检查按钮已接入 |
| 检查本轮更新 / 运行日志 / 多账号并行(③/⑤) | ③ 提供「检查本轮更新」按钮,不打开 Shopee、不提交、不改任务状态,只显示当前筛选范围、店铺分布、预计分批和会更新字段;点击「检查本轮更新」或「开始更新」时清空③界面旧日志,只显示本轮检查/更新日志;真实更新写运行日志;可在⑤开启多账号并行,同账号内仍串行 | V2 已接入,检查按钮已接入 |
| 结果存储与回写 | 各阶段结果实时存 SQLite;该文件全部完成后把旧/新数据+状态批量回写原 Excel | P0 |
| 设置(⑤) | AI 模型管理(下拉+新增/删除/详情/测试连接,至少各一个文本+图像模型);标题/图片大模型角色选择;分辨率(512/1k/2k/4k,返回超时随分辨率自动);并发/重试/jpg质量;图片目录/Chrome 路径/端口;设置页采用居中内容区、适度左右留白和响应式三列表单,长字段跨列显示;点击「保存设置」成功后弹轻量提示框 | P0 |
| 首次引导保护 | 未配账号、对应账号 Chrome 未启动或未登录时,① ③ 执行按钮禁用/执行前拦截并提示去④;③ 不自动打开缺失账号 Chrome,必须中止本轮更新 | P0 |
+2
View File
@@ -78,6 +78,7 @@
| T-404a | ②/③ 选中记录重置(联调重试辅助) | T-303, T-403, T-504 | 现状:② AI生成和③ 更新shopee列表没有重置选中记录的功能,生成/上传/提交成功后难以用同一商品重复测试。方案:② 增加「重置生成结果」按钮,仅作用当前选中单条,确认后清空本地 `new_title/new_cover_path`、清空 `last_error`,把任务退回 `stage=collected/status=success`,默认不删除本地新封面文件,可在确认框提示路径;③ 增加「重置更新状态」按钮,仅作用当前选中单条,保留 `new_title/new_cover_path`,确认后把本地状态退回可更新(`stage=generated/status=pending`、清空 `last_error`),用于重复测试上传/提交。若记录 `committed=1`,确认框必须醒目提示“线上已提交过,本地重置不会回滚 Shopee,重复更新会再次提交线上”,并保留 committed 历史事实/运行日志;运行中禁用;两类重置都写 `run_type=reset` 运行日志,不触碰 Shopee、不回写 Excel,后续由手动回写同步本地状态 | DONE |
| T-404 | 真实 Shopee 单条更新冒烟验收 | T-403, T-501c | 已完成真实验收:2026-07-01 用户手动导入包含 5 个商品 ID 的 Excel,完成 AI 标题/图片生成,并通过 Tab③「开始更新」流程更新到 Shopee;覆盖真实编辑页、标题/封面更新、Shopee 站点侧确认框、提交后结果判断与本地状态链路。后续若 Shopee 页面结构变化,仍需按 CDP 规则补回归验证 | DONE |
## Phase 5 · 设置与收尾
| ID | 任务 | 依赖 | 验收要点 | 状态 |
@@ -112,6 +113,7 @@
| T-521 | 依赖清单(锁版本 requirements) | T-006 | 依据 `docs/engineering-review.md` P0。新增锁版本的 `requirements.txt`(或 `pyproject.toml`)声明 PySide6/openpyxl/websocket-client/requests 及版本,与 `docs/03-tech-stack.md` 依赖纪律对齐;README/文档补安装说明;不引入新运行时依赖、不改业务代码 | DONE |
| T-522 | CI:自动跑语法 + 单元/ GUI 测试 | T-006, T-521 | 依据 `docs/engineering-review.md` P1。加 GitHub Actions,在 push/PR 上按 `requirements.txt` 安装依赖并跑 `python -m compileall app main.py` + `python -m unittest discover -s tests`(含 PySide6 环境下的 GUI 测试,`QT_QPA_PLATFORM=offscreen`);不连真实 Shopee/AI;失败即红灯,把"改完必跑测试"变强制门禁 | DONE |
| T-523 | 拆分 `app/gui.py` 为 `app/gui/` 包 | T-511, T-514, T-515, T-516, T-517 | 依据 `docs/engineering-review.md` P0。把 6317 行 God-file 拆为包:`models.py`(3 个 TableModel)、`tabs/`(①~⑤各一文件)、`workers.py`(Generate/Apply/Collect/WriteBack/AccountLoginCheck/AIModelTest 从 gui 挪出,与 `app/workers.py` 基类归拢)、`widgets.py`(色板常量、空状态卡、批次总览等 helper)、`main_window.py`。纯结构重构、对外行为与公开符号不变(保留 `from app import gui` 及 `MainWindow`/各 Tab/Worker 的导入路径或提供兼容再导出),保留 PySide6 缺失优雅降级;68 个 GUI 测试全绿、不删减断言 | DONE |
| T-523a | ②/③ 新一轮运行前清空界面日志显示 | T-519, T-505, T-523 | 问题:②AI生成和③更新shopee进入模块或启动新一轮任务时仍显示上一次运行日志,用户容易把旧失败、旧进度误认为本轮状态。方案:点击②「开始生成」、③「检查本轮更新」或③「开始更新」时,先清空对应界面的可见日志显示区,并写入本轮开始摘要;运行中只追加本轮日志,完成后保留本轮日志。只清空界面文本,不删除 SQLite `run_logs/run_log_events` 或本地 `logs/` 诊断日志;进入页面默认可显示“本轮日志会在开始运行后显示”,历史日志查看作为后续低优先级入口,不自动混入当前运行界面。同步 GUI 测试覆盖新一轮运行不会混入旧日志 | DONE |
| T-524 | PyInstaller 打包为免安装 exe | T-521 | 依据 `docs/engineering-review.md` P1。新增 PyInstaller spec/脚本,产出 Windows 免安装 `.exe`;打包排除并绝不内置 `config.json`/`config/ai_models.json`/`cmshopee.db*`/`chrome_user_data_dir/`/`images/`/`logs/` 等本地数据与密钥;首次运行按现有默认值在本地生成配置;文档补打包与分发步骤 | TODO |
| T-525 | 引入 ruff(lint + format)+ 可选 pre-commit | T-006 | 依据 `docs/engineering-review.md` P1。加 `ruff` 配置(lint + format),先以现状为基线不做大规模风格重排,只开启安全规则(未用 import/变量、明显错误);可选 `.pre-commit-config.yaml`;不改业务逻辑;CI(T-522)可串入 ruff 检查。数据模型渐进上 mypy 作为后续可选 | TODO |
+5 -2
View File
@@ -256,6 +256,7 @@ 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 保留现场。Shopee 确认成功后可能把当前 tab 跳回 `/portal/product/list/all?operationSortBy=modified_time`,`click_update()` 会把该 URL 记录到 `post_update.url` 并标记 `redirected_to_list=true`;若本次会关闭该自动新开 tab,关闭前等待 2 秒。
- `click_update()` 的提交成功定义:页面主「更新」按钮已点击,且 Shopee 站点侧确认框未出现或已在可见 `.eds-modal__content` / `.eds-modal__box` 内点击主按钮「更新」。如果确认框仍停留、只点到页面主按钮、或误入「立即優化」,必须返回失败并保留现场。
@@ -381,6 +382,7 @@ T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负
- 别名未匹配 `accounts.alias` 时列表阶段列显示“略过”;点击「采集旧标题/旧封面」后由 `CollectWorker` 逐条写库为 `skipped`,原因 `别名未匹配账号`。
- 「采集旧标题/旧封面」通过 `CollectWorker` 后台执行,只处理 `stage=imported` 的任务;采集前先做账号就绪预检。无账号、当前批次匹配账号未启动 CDP 端口或未登录时,返回 `blocked=True`,GUI 弹窗汇总并跳转/引导去④账号管理,不进入逐条采集、不写 skipped/failed。预检通过后,已匹配任务调用 `editor.collect()` 下载旧封面到 `image_dir/<batch_id>/<slug>/<task_id>_<item_id>_old.jpg` 并 `db.set_collected()`;别名未匹配任务仍逐条 `mark_skipped`;单条失败 `mark_failed(..., "collect", error)` 后继续。采集完成且本轮成功采集数量大于 0 时,自动触发当前批次旧字段回写;锁文件失败时只提示,不回滚 SQLite。
- 采集打开商品页时,若本轮自动新建 tab,采集完成/失败后会关闭该 tab;若复用用户已打开的商品页,只断开 CDP 连接不关闭页面。
- 「停止」调用 worker 的协作式 `cancel()`,已开始的单条跑到安全边界后结束。
- 「回写旧数据到 Excel」通过 `WriteBackWorker` 后台调用 `excel.write_back()`,把已采集旧标题/旧封面路径按原 Excel 行定位写回;该按钮主要作为自动回写失败后的手动重试入口。原文件被占用时弹窗提示关闭后重试,SQLite 采集结果不回滚。
- 采集前由 `CollectWorker` 做账号就绪预检:无账号、当前批次匹配账号未启动 CDP 端口或未登录时,返回 `blocked=True`,GUI 弹窗汇总并跳转/引导去④账号管理;不无提示批量启动所有账号 Chrome。匹配账号未登录属于预检阻断,不是逐条 skipped。
@@ -394,7 +396,7 @@ T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负
- 筛选栏包含:批次、店铺、商品ID、状态、刷新。批次来自 `db.list_batches()`;店铺来自当前任务别名并优先显示匹配账号名;商品ID输入框按包含匹配 `item_id`,清空表示全部;状态支持全部/待生成/已生成/失败/略过/已更新。
- 任务列表使用 `QTableView + GenerateTaskTableModel`,列为:店铺、商品ID、旧标题、新标题、状态。`stage=collected` 显示“待生成”,`stage=generated` 显示“已生成”,`status=failed/skipped/running` 优先显示对应状态;已生成、未提交线上、非运行中的「新标题」列可双击编辑,调用 `db.update_generated_title()` 写回本地并清空 `last_error`。
- 双击「新标题」列进入本地编辑;双击其他列弹窗展示旧封面与新封面路径对应图片;图片不存在时显示空态/路径提示,只做查看,不做审核。
- 底部「开始生成」只处理当前筛选结果里 `stage=collected` 的任务;批次/店铺/商品ID/状态筛选共同决定当前筛选结果;通过 `GenerateWorker` 调 `ai.generate_batch()`,先并发标题;只有②「生成封面图片(成本较高)」勾选时才继续并发封面。
- 底部「开始生成」只处理当前筛选结果里 `stage=collected` 的任务;批次/店铺/商品ID/状态筛选共同决定当前筛选结果;点击「开始生成」时先清空 `GenerateTab` 可见日志文本并写入本轮开始摘要,后续只追加本轮日志;通过 `GenerateWorker` 调 `ai.generate_batch()`,先并发标题;只有②「生成封面图片(成本较高)」勾选时才继续并发封面。
- 「停止」调用 worker 的协作式 `cancel()`;未开始的 Future 取消,不记失败;已完成的 `db.set_generated()` 结果保留,可再次开始生成剩余任务。
- 进度标签显示标题/图片/失败;未勾选生成封面时图片进度显示本轮未生成,标题成功即 `db.set_generated(task_id, new_title, None)` 并进入 generated;勾选时每条封面生成成功后立即 `db.set_generated(task_id, new_title, new_cover_path)`。失败 `db.mark_failed(..., "generate", error)`,GUI 刷新任务表。
- T-404a 已实现:「重置生成结果」读取当前选中单条;确认后调用 DB reset,清空本地 AI 结果并退回可生成状态;默认不删除本地新封面文件;写 `run_type=reset` 事件。
@@ -413,7 +415,8 @@ T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负
- 检查本轮更新:不做账号登录预检,不调用 `editor.apply_task()`,不写任务状态,不回写 Excel;只把每条“将更新/将略过”写入运行日志并弹汇总。
- 真实更新前先做账号就绪预检:无账号、当前筛选结果匹配账号 Chrome 未启动、CDP 端口不可访问、未登录,或本轮涉及账号调试端口冲突时,返回 `blocked=True`,GUI 弹窗汇总并跳转/引导去④账号管理;预检不通过时不调用 `editor.apply_task()`、不写失败状态、不自动启动 Chrome。
- 预检通过后默认串行;若 `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`,单条失败继续下一条。
- 检查和真实更新都会创建 `run_logs`,并把逐条事件写入 `run_log_events`;③ 页面显示最近运行日志。
- 检查和真实更新都会创建 `run_logs`,并把逐条事件写入 `run_log_events`;点击「检查本轮更新」或「开始更新」时先清空 `ApplyTab` 可见日志文本并写入本轮开始摘要,后续只追加本轮日志;③ 页面不自动把上一轮历史日志混入当前运行界面。
- 若 `close_success_tab=true`,`editor.apply_task()` 只关闭本轮自动新开且成功提交的商品页;确认后跳回商品列表页时,关闭前等待 2 秒;失败和复用的用户已有 tab 不关闭。
- 别名未匹配账号的任务逐条 `db.mark_skipped()`,原因 `别名未匹配账号`;「停止」调用 worker 协作式 `cancel()`,已开始单条跑到安全边界后结束。
- T-404a/T-508 已实现:「重置更新状态」从底部批处理按钮移到任务表右键菜单,读取当前选中单条,运行中禁用;确认后保留 `new_title/new_cover_path`,本地退回 `stage=generated/status=pending` 供重复更新;`committed=1` 时必须提示线上已提交过且不回滚 Shopee,并保留 committed 历史事实/运行日志。
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -54,6 +54,7 @@
- 导入:openpyxl 解析**输入列**(账号名/别名/商品id)入 SQLite。
- **导入汇总栏**(导入后即时刷新,跑采集前的校验关口):显示 文件数、解析行数(原始数据量)、有效/无效行、匹配账号行数(按账号细分)、未匹配行数。未匹配/无效数字标红可点,点击在列表筛出便于定位纠错。
- 采集:用该账号已启动并登录的 Chrome 只读打开商品页,读旧标题、下载旧封面到 `images/<batch_id>/<slug>/<task_id>_<item_id>_old.jpg`,写 `old_title/old_cover_path`,stage=collected。若程序为采集自动新建商品页 tab,采集结束后自动关闭;若复用用户原本打开的 tab,则不关闭。
- 回写:采集完成后自动把旧标题/旧封面路径批量回写原 Excel;保留「回写旧数据到 Excel」作为手动重试入口(原文件被锁→提示关闭后重试/另存)。
- 别名未匹配账号 / 账号未登录 → 该行 skipped 并记原因。`T-207` 接入后,① 采集会像③更新一样写 `run_logs/run_log_events`,并把完整脱敏 traceback 写入本地 `logs/`,用于定位失败卡在哪个步骤。
@@ -86,7 +87,7 @@
- 右下:任务列表(店铺名、商品id、旧标题、新标题、状态)+ AI生成运行日志;已生成、未提交线上、非运行中的任务可双击「新标题」列本地微调,写回 `tasks.new_title`,清空 `last_error` 并回到可更新;双击其他列弹窗展示旧封面 | 新封面(纯查看)。
- 底部**单个「开始生成」+「停止」**,并增加「生成封面图片(成本较高)」复选框:默认不勾选,只生成标题;勾选后才在标题完成后按 `image_concurrency` 并发生成封面。该开关状态持久化到 `config.json` 的 `ai.generate_cover`,但入口放在②,便于用户在每轮生成前做成本判断。
- 生成参数(标题/图片并发数、失败重试、分辨率、jpg 质量、模型/Key)在 **⑤ 设置**;②只暴露“本轮是否生成封面”的成本开关。
- 标题-only 模式标题成功即写库并进入 `generated`,`new_cover_path` 留空;标题+封面模式图片成功后写入本地新封面路径。两种模式都写 `run_type=generate` 的 `run_logs/run_log_events` 和用户可读滚动日志;未勾选生成封面时日志明确显示“本轮仅生成标题”。「停止」取消未开始项,可再次「开始生成」对剩余继续。
- 标题-only 模式标题成功即写库并进入 `generated`,`new_cover_path` 留空;标题+封面模式图片成功后写入本地新封面路径。两种模式都写 `run_type=generate` 的 `run_logs/run_log_events` 和用户可读滚动日志;未勾选生成封面时日志明确显示“本轮仅生成标题”。点击「开始生成」时先清空②界面可见日志并写入本轮开始摘要,运行中只追加本轮日志;不删除历史 `run_logs/run_log_events` 或本地 `logs/`。进入页面默认可显示“本轮日志会在开始运行后显示”,历史日志不自动混入当前运行界面。「停止」取消未开始项,可再次「开始生成」对剩余继续。
- 「重置生成结果」仅作用当前选中单条,运行中禁用;确认后只改本地 DB,清空 `new_title/new_cover_path/last_error` 并退回 `collected/success` 供重新生成,默认不删除本地新封面文件。若该记录已提交过线上,必须在确认框提示本地重置不回滚 Shopee。
- **无逐条人工审核环节**;新标题默认直接用 AI 输出,但允许对已生成且未提交线上的单行做本地微调;可选对单行 `重生成`。生成完即可进入 ③,③ 开始更新前会做批量确认。
@@ -112,10 +113,11 @@
- 安全开关通过后,弹窗展示本次筛选条件、任务总数、每批最大条数、预计批次数、安全设置和“将提交线上”的风险提示;用户点「是/确认」才开始,点「否/取消」不执行。
- 真实更新第一条商品前做账号就绪预检:按当前筛选结果汇总需要的账号;无账号、Chrome 未启动、CDP 端口不可访问、未登录或端口冲突时,弹窗列出具体账号/原因并中止本轮,不自动调用「启动登录」或静默打开 Chrome。
- 对确认后的**已生成(generated)任务**执行:打开编辑页换标题+换封面 → 点页面「更新」 → 如 Shopee 弹出“確定您要更新商品嗎?”确认框(`.eds-modal__content` / `.eds-modal__box`),则只点弹窗主按钮「更新」提交,不点「立即優化」。
- 更新封面时统一按替换第一张执行:删除第一张前必须已有该任务的本地旧封面备份(①采集得到的 `old_cover_path` 且文件存在);备份缺失时阻断该条更新并提示先采集/修复备份,不盲删线上图片。
- 默认串行、单条失败继续;⑤ 可开启多账号并行,不同账号同时执行,同一账号内仍串行;真实更新前若本轮账号调试端口冲突则阻断。
- 「检查本轮更新」只写运行日志与检查汇总,不打开 Shopee、不调用 `editor.apply_task()`、不写任务状态、不回写 Excel。
- 真实更新每条立即写回 SQLite(committed/状态/error),失败不阻塞后续任务;检查和真实更新都会写 `run_logs/run_log_events`。
- 真实更新每条立即写回 SQLite(committed/状态/error),失败不阻塞后续任务;检查和真实更新都会写 `run_logs/run_log_events`。点击「检查本轮更新」或「开始更新」时先清空③界面可见日志并写入本轮检查/更新开始摘要,运行中只追加本轮日志;不删除历史 `run_logs/run_log_events` 或本地 `logs/`,历史日志不自动混入当前运行界面。
- 分批更新时「停止」为协作式停止:已开始的当前商品跑到安全边界并写库后停止,不再开始新商品、不进入下一批;未开始任务保持原状态,下次可继续。
- 更新前做账号就绪预检:无账号、对应账号 Chrome 未启动、CDP 端口不可访问或未登录时整体阻断并引导去④账号管理,不进入逐条提交,也不自动打开账号 Chrome。
- 若 ⑤ 开启“成功后关闭本次新开编辑页”,则仅关闭本轮程序自动新开且成功提交的商品页;确认后跳回我的商品列表页时,关闭前等待 2 秒;失败任务和用户原本打开的 tab 不关闭。
+12 -1
View File
@@ -1034,4 +1034,15 @@
- 代码:删除旧 `app/gui.py`,新增 `app/gui/` 包;`__init__.py` 保持 `from app import gui`、`from app.gui import MainWindow/各 Tab/各 Worker` 等旧导入兼容;`main_window.py` 放 `MainWindow`;`models.py` 放 3 个 TableModel;`widgets.py` 放 Qt 导入、语义色板、空状态卡、批次总览和日志 helper;`workers.py` 放 `GenerateWorker`/`ApplyWorker`/`CollectWorker`/`WriteBackWorker`/`AccountLoginCheckWorker`/`AIModelTestWorker`;`tabs/` 按 ①~⑤ 拆分各 Tab。
- 边界:纯结构重构;未改 Shopee/CDP、AI、DB、Excel 行为;PySide6 缺失时仍由 `main()` 返回 1 并提示,旧公开导入路径继续可用。
- 文档:同步 `docs/04-architecture.md`、`docs/api.md`、`docs/current-state.md`;`docs/06-tasks.md` 将 T-523 标为 DONE,下一个可领取任务更新为 T-524。
- 验证:`python -m compileall app main.py` 通过;`python -m unittest discover -s tests -p "test_gui.py"` 通过(70 tests);`python -m unittest discover -s tests` 通过(159 tests)。
- 验证:`python -m compileall app main.py` 通过;`python -m unittest discover -s tests -p "test_gui.py"` 通过(70 tests);`python -m unittest discover -s tests` 通过(159 tests)。
## 【2026-07-02】文档 · T-523a ②/③ 本轮日志显示重置
- 需求:②AI生成和③更新shopee启动新一轮运行时,不应继续显示上一轮日志,避免用户把旧失败、旧进度误认为本轮状态。
- 方案:新增 T-523a;点击②「开始生成」、③「检查本轮更新」或③「开始更新」时,先清空对应界面的可见日志显示区,并写入本轮开始摘要;运行中只追加本轮日志,完成后保留本轮日志。只清空界面文本,不删除 SQLite `run_logs/run_log_events` 或本地 `logs/` 诊断日志。
- 文档:同步 `docs/02-requirements.md`、`docs/routes.md`、`docs/api.md`、`docs/06-tasks.md`、`docs/current-state.md`;当前下一个可领取任务改为 T-523a。
- 验证:文档-only 更新,未运行单元测试。
## 【2026-07-02】T-523a 完成 · ②/③ 本轮日志显示重置
- 代码:`GenerateTab` 和 `ApplyTab` 进入页面后不再自动加载上一轮历史日志,默认显示“本轮日志会在开始运行后显示”;②点击「开始生成」时清空可见日志并写入本轮 AI 生成摘要;③点击「检查本轮更新」或「开始更新」并确认后清空可见日志并写入本轮检查/更新摘要。
- 边界:只清空 GUI 文本显示,不删除 SQLite `run_logs/run_log_events` 或本地 `logs/`;未改 AI HTTP、DB schema、Excel、Shopee/CDP 更新流程。
- 测试:更新 `tests/test_gui.py`,覆盖②/③进入页面不自动混入历史日志,以及②开始生成、③检查/真实更新启动时旧日志被清掉且本轮摘要出现。
- 文档:`docs/06-tasks.md` 将 T-523a 标为 DONE;同步 `docs/current-state.md`,下一个可领取任务更新为 T-524。
- 验证:`python -m unittest discover -s tests -p "test_gui.py"` 通过(72 tests)。
+93 -2
View File
@@ -1150,7 +1150,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_generate_tab_loads_latest_generate_run_log(self):
def test_generate_tab_does_not_auto_mix_latest_generate_run_log(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
db.init_db(cfg["db_path"])
@@ -1173,11 +1173,94 @@ class GuiTests(TempDirMixin, unittest.TestCase):
tab = GenerateTab(config=cfg)
self.addCleanup(tab.close)
text = tab.run_log_view.toPlainText()
self.assertEqual("generateRunLogView", tab.run_log_view.objectName())
self.assertIn("phase=cover step=cover_request result=failed", tab.run_log_view.toPlainText())
self.assertIn("本轮日志会在开始运行后显示", text)
self.assertNotIn("phase=cover step=cover_request result=failed", text)
self.assert_removed(temp_dir)
def test_generate_tab_start_generate_resets_visible_log_for_current_run(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"])
tab = GenerateTab(config=cfg)
self.addCleanup(tab.close)
tab.run_log_view.setPlainText("上一轮失败日志")
class FakeSignal:
def __init__(self):
self.callbacks = []
def connect(self, callback):
self.callbacks.append(callback)
class FakeThread:
def __init__(self):
self.finished = FakeSignal()
self.started = False
def start(self):
self.started = True
fake_thread = FakeThread()
with mock.patch("app.gui.run_worker", return_value=fake_thread):
tab.start_generate()
text = tab.run_log_view.toPlainText()
self.assertTrue(fake_thread.started)
self.assertNotIn("上一轮失败日志", text)
self.assertIn("本轮AI生成开始:任务 1 条", text)
self.assertIn("生成封面:否", text)
self.assert_removed(temp_dir)
def test_apply_tab_does_not_auto_mix_latest_apply_run_log(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
db.init_db(cfg["db_path"])
run_id = db.create_run_log("apply", total=1, path=cfg["db_path"])
db.add_run_log_event(
run_id,
"step=apply_update result=failed detail=上一轮更新失败",
level="error",
path=cfg["db_path"],
)
db.finish_run_log(
run_id,
status="done",
done=1,
failed_count=1,
summary_json={"failed": 1},
path=cfg["db_path"],
)
tab = ApplyTab(config=cfg)
self.addCleanup(tab.close)
text = tab.run_log_view.toPlainText()
self.assertEqual("applyRunLogView", tab.run_log_view.objectName())
self.assertIn("本轮日志会在开始运行后显示", text)
self.assertNotIn("step=apply_update result=failed", text)
self.assert_removed(temp_dir)
def test_apply_tab_lists_generated_tasks_and_filters_by_batch_shop_status(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
@@ -1437,6 +1520,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
statuses = []
tab = ApplyTab(config=cfg, status_callback=statuses.append)
self.addCleanup(tab.close)
tab.run_log_view.setPlainText("上一轮更新失败")
class FakeSignal:
def __init__(self):
@@ -1469,6 +1553,9 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertFalse(tab.apply_worker.dry_run)
self.assertFalse(tab.apply_worker.parallel_accounts)
self.assertEqual(1, tab.apply_worker.batch_size)
log_text = tab.run_log_view.toPlainText()
self.assertNotIn("上一轮更新失败", log_text)
self.assertIn("本轮更新开始:任务 1 条,每批 1 条", log_text)
self.assertFalse(tab.preview_update_button.isEnabled())
self.assertFalse(tab.start_update_button.isEnabled())
self.assertTrue(tab.stop_update_button.isEnabled())
@@ -1515,6 +1602,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
statuses = []
tab = ApplyTab(config=cfg, status_callback=statuses.append)
self.addCleanup(tab.close)
tab.run_log_view.setPlainText("上一轮检查失败")
class FakeSignal:
def __init__(self):
@@ -1543,6 +1631,9 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertTrue(tab.apply_worker.dry_run)
self.assertTrue(tab.apply_worker.parallel_accounts)
self.assertEqual(2, tab.apply_worker.max_parallel_accounts)
log_text = tab.run_log_view.toPlainText()
self.assertNotIn("上一轮检查失败", log_text)
self.assertIn("本轮检查开始:任务 1 条,每批 1 条", log_text)
self.assertEqual("开始检查本轮更新:1 条", statuses[-1])
self.assert_removed(temp_dir)