feat: 完成T-204b采集自动回写

采集完成且本轮有成功采集任务时,Tab① 自动启动 WriteBackWorker 回写旧标题和旧封面路径到当前批次 Excel。

保留手动回写旧数据到 Excel 作为自动回写失败后的重试入口;原 Excel 被占用时提示关闭文件后手动重试,SQLite 采集结果不回滚。

新增 GUI 测试覆盖自动触发、无成功采集不触发、锁文件提示手动重试;同步 T-204b 任务状态、current-state、api、architecture 与 progress。
This commit is contained in:
chengma
2026-06-27 14:47:08 +08:00
parent b9d84e71ec
commit d7e609434e
7 changed files with 131 additions and 26 deletions
+43 -11
View File
@@ -349,16 +349,34 @@ if QT_IMPORT_ERROR is None:
if not batch_id: if not batch_id:
self._set_status("没有可回写批次") self._set_status("没有可回写批次")
return return
self._start_write_back(batch_id)
def _start_write_back(self, batch_id, auto=False):
if self.write_back_thread is not None:
self._set_status("Excel 回写正在进行...")
return False
worker = WriteBackWorker(batch_id, db_path=self.db_path) worker = WriteBackWorker(batch_id, db_path=self.db_path)
worker.failed.connect(self._on_write_back_failed) worker.failed.connect(
worker.finished.connect(self._on_write_back_finished) lambda task_id, error, auto=auto: self._on_write_back_failed(
task_id,
error,
auto=auto,
)
)
worker.finished.connect(
lambda payload, auto=auto: self._on_write_back_finished(
payload,
auto=auto,
)
)
thread = run_worker(worker, thread_name="WriteBackWorker", start=False) thread = run_worker(worker, thread_name="WriteBackWorker", start=False)
thread.finished.connect(lambda: self._forget_write_back_thread(thread)) thread.finished.connect(lambda: self._forget_write_back_thread(thread))
self.write_back_worker = worker self.write_back_worker = worker
self.write_back_thread = thread self.write_back_thread = thread
self._set_write_back_running(True) self._set_write_back_running(True)
self._set_status("正在回写旧数据到 Excel...") self._set_status("正在自动回写旧数据到 Excel..." if auto else "正在回写旧数据到 Excel...")
thread.start() thread.start()
return True
def _active_batch_id(self): def _active_batch_id(self):
if self.current_batch_id: if self.current_batch_id:
@@ -415,13 +433,22 @@ if QT_IMPORT_ERROR is None:
def _on_collect_finished(self, payload): def _on_collect_finished(self, payload):
self._set_collect_running(False) self._set_collect_running(False)
self.refresh_tasks() self.refresh_tasks()
self._set_status( message = "采集完成:成功{collected},略过{skipped},失败{failed}".format(
"采集完成:成功{collected},略过{skipped},失败{failed}".format(
collected=payload.get("collected", 0), collected=payload.get("collected", 0),
skipped=payload.get("skipped", 0), skipped=payload.get("skipped", 0),
failed=payload.get("failed", 0), failed=payload.get("failed", 0),
) )
) if payload.get("collected", 0) > 0:
batch_id = self._active_batch_id()
if batch_id and self._start_write_back(batch_id, auto=True):
self._set_status(f"{message},正在自动回写 Excel...")
return
if not batch_id:
self._set_status(f"{message},但没有可回写批次")
return
self._set_status(f"{message},Excel 回写已在进行")
return
self._set_status(message)
def _on_collect_cancelled(self, payload): def _on_collect_cancelled(self, payload):
self._set_collect_running(False) self._set_collect_running(False)
@@ -433,22 +460,27 @@ if QT_IMPORT_ERROR is None:
) )
) )
def _on_write_back_failed(self, task_id, error): def _on_write_back_failed(self, task_id, error, auto=False):
message = f"Excel 回写失败:{error}" message = f"Excel {'自动' if auto else ''}回写失败:{error}"
if "被占用" in str(error): if "被占用" in str(error):
if auto:
message += "\n请关闭原 Excel 后点击「回写旧数据到 Excel」手动重试;SQLite 已保留采集结果,也可另存副本。"
else:
message += "\n请关闭原 Excel 后重试;SQLite 已保留采集结果,也可另存副本。" message += "\n请关闭原 Excel 后重试;SQLite 已保留采集结果,也可另存副本。"
QMessageBox.warning(self, "回写旧数据", message) QMessageBox.warning(self, "回写旧数据", message)
self._set_status(message.replace("\n", " ")) self._set_status(message.replace("\n", " "))
def _on_write_back_finished(self, payload): def _on_write_back_finished(self, payload, auto=False):
self._set_write_back_running(False) self._set_write_back_running(False)
if payload.get("ok") is False: if payload.get("ok") is False:
error = payload.get("error") or "未知错误" error = payload.get("error") or "未知错误"
self._set_status(f"Excel 回写失败:{error}") retry_hint = ",可点击「回写旧数据到 Excel」手动重试" if auto else ""
self._set_status(f"Excel {'自动' if auto else ''}回写失败:{error}{retry_hint}")
return return
self.refresh_tasks() self.refresh_tasks()
self._set_status( self._set_status(
"Excel 回写完成:文件{files},行{rows}".format( "Excel {prefix}回写完成:文件{files},行{rows}".format(
prefix="自动" if auto else "",
files=payload.get("files", 0), files=payload.get("files", 0),
rows=payload.get("rows", 0), rows=payload.get("rows", 0),
) )
+1 -1
View File
@@ -288,7 +288,7 @@ images/<slug>/<item_id>_new.<ext> # AI 生成的新封面
- 用账号 Chrome 打开商品页,等就绪,读旧标题(标题输入框 value)。 - 用账号 Chrome 打开商品页,等就绪,读旧标题(标题输入框 value)。
- 旧封面:取第一张 itembox 的 `img.src`(CDN 链接),下载到 `images/<slug>/<item_id>_old`。 - 旧封面:取第一张 itembox 的 `img.src`(CDN 链接),下载到 `images/<slug>/<item_id>_old`。
- 写 `old_title/old_cover_path`、stage=collected;批量回写 Excel 旧字段。 - 写 `old_title/old_cover_path`、stage=collected;批量回写 Excel 旧字段。
- T-204b 目标:采集任务结束后自动触发当前批次旧字段回写;原 Excel 被锁时不影响 SQLite 结果,提示关闭后重试,并保留手动「回写旧数据到 Excel」入口。 - 采集任务结束且本轮有成功采集行时,自动触发当前批次旧字段回写;原 Excel 被锁时不影响 SQLite 结果,提示关闭后重试,并保留手动「回写旧数据到 Excel」入口。
### 6.2 AI 生成(② Tab) ### 6.2 AI 生成(② Tab)
+1 -1
View File
@@ -52,7 +52,7 @@
| T-202b | Tab① 导入汇总栏 | T-202 | 导入后显示 文件数/解析行数/有效/无效/匹配(按账号)/未匹配;未匹配可点击筛出 | DONE | | T-202b | Tab① 导入汇总栏 | T-202 | 导入后显示 文件数/解析行数/有效/无效/匹配(按账号)/未匹配;未匹配可点击筛出 | DONE |
| T-203 | 采集旧标题+旧封面(只读),下载图片,立即写库 | T-202, T-001, T-104b | 通过 worker 执行;逐条 set_collected;旧封面下载到 `images/<slug>/`;未登录/未匹配略过记原因 | DONE | | T-203 | 采集旧标题+旧封面(只读),下载图片,立即写库 | T-202, T-001, T-104b | 通过 worker 执行;逐条 set_collected;旧封面下载到 `images/<slug>/`;未登录/未匹配略过记原因 | DONE |
| T-204 | 回写旧字段到原 Excel(含文件锁处理) | T-203, T-201 | `excel.write_back()` 按源文件/工作表/行号回写旧标题、旧封面;`export_copy()` 另存副本;Tab① 用 `WriteBackWorker` 后台回写,文件被占用时提示关闭后重试 | DONE | | T-204 | 回写旧字段到原 Excel(含文件锁处理) | T-203, T-201 | `excel.write_back()` 按源文件/工作表/行号回写旧标题、旧封面;`export_copy()` 另存副本;Tab① 用 `WriteBackWorker` 后台回写,文件被占用时提示关闭后重试 | DONE |
| T-204b | 采集完成后自动回写旧字段到 Excel | T-204 | `CollectWorker` 完成后自动触发 `excel.write_back()` 回写当前批次旧字段;成功时状态栏/日志提示“已回写”;原文件被锁时不影响 SQLite,提示关闭后点「回写旧数据到 Excel」手动重试或另存副本 | TODO | | T-204b | 采集完成后自动回写旧字段到 Excel | T-204 | `CollectWorker` 完成后自动触发 `excel.write_back()` 回写当前批次旧字段;成功时状态栏/日志提示“已回写”;原文件被锁时不影响 SQLite,提示关闭后点「回写旧数据到 Excel」手动重试或另存副本 | DONE |
| T-205 | 首次未配账号 / Chrome 未启动 / 未登录的引导保护 | T-105, T-203 | 无账号、匹配账号未启动 CDP 端口或未登录时,① 执行按钮禁用或采集前汇总提示,并引导去④;可提供“打开账号管理/启动登录”入口,但不无提示批量启动所有账号 Chrome | TODO | | T-205 | 首次未配账号 / Chrome 未启动 / 未登录的引导保护 | T-105, T-203 | 无账号、匹配账号未启动 CDP 端口或未登录时,① 执行按钮禁用或采集前汇总提示,并引导去④;可提供“打开账号管理/启动登录”入口,但不无提示批量启动所有账号 Chrome | TODO |
## Phase 3 · AI 生成(②) ## Phase 3 · AI 生成(②)
+2 -2
View File
@@ -301,9 +301,9 @@ TAB_STYLE: str # 顶层 Tab 栏防误点样式:
- 任务列表使用 `QTableView + TaskTableModel`,列为:账号、别名、商品ID、阶段。 - 任务列表使用 `QTableView + TaskTableModel`,列为:账号、别名、商品ID、阶段。
- 账号列优先显示匹配到的 `accounts.account_name`;未匹配账号时保留 Excel 输入账号名。 - 账号列优先显示匹配到的 `accounts.account_name`;未匹配账号时保留 Excel 输入账号名。
- 别名未匹配 `accounts.alias` 时列表阶段列显示“略过”;点击「采集旧标题/旧封面」后由 `CollectWorker` 写库为 `skipped`,原因 `别名未匹配账号`。 - 别名未匹配 `accounts.alias` 时列表阶段列显示“略过”;点击「采集旧标题/旧封面」后由 `CollectWorker` 写库为 `skipped`,原因 `别名未匹配账号`。
- 「采集旧标题/旧封面」通过 `CollectWorker` 后台执行,只处理 `stage=imported` 的任务;每条先检测登录,未登录写 `mark_skipped`,已登录则下载旧封面到 `image_dir/<slug>/<item_id>_old.jpg` 并 `db.set_collected()`;单条失败 `mark_failed(..., "collect", error)` 后继续。T-204b 后采集完成应自动触发当前批次旧字段回写,锁文件失败时只提示,不回滚 SQLite。 - 「采集旧标题/旧封面」通过 `CollectWorker` 后台执行,只处理 `stage=imported` 的任务;每条先检测登录,未登录写 `mark_skipped`,已登录则下载旧封面到 `image_dir/<slug>/<item_id>_old.jpg` 并 `db.set_collected()`;单条失败 `mark_failed(..., "collect", error)` 后继续。采集完成且本轮成功采集数量大于 0 时,自动触发当前批次旧字段回写;锁文件失败时只提示,不回滚 SQLite。
- 「停止」调用 worker 的协作式 `cancel()`,已开始的单条跑到安全边界后结束。 - 「停止」调用 worker 的协作式 `cancel()`,已开始的单条跑到安全边界后结束。
- 「回写旧数据到 Excel」通过 `WriteBackWorker` 后台调用 `excel.write_back()`,把已采集旧标题/旧封面路径按原 Excel 行定位写回;T-204b 后该按钮主要作为自动回写失败后的手动重试入口。原文件被占用时弹窗提示关闭后重试,SQLite 采集结果不回滚。 - 「回写旧数据到 Excel」通过 `WriteBackWorker` 后台调用 `excel.write_back()`,把已采集旧标题/旧封面路径按原 Excel 行定位写回;该按钮主要作为自动回写失败后的手动重试入口。原文件被占用时弹窗提示关闭后重试,SQLite 采集结果不回滚。
- T-205 后,若无账号、当前批次匹配账号未启动 CDP 端口或未登录,① 的采集执行应禁用或在执行前汇总拦截并提示去④账号管理;可以提供跳转/启动登录入口,但不无提示批量启动所有账号 Chrome。 - T-205 后,若无账号、当前批次匹配账号未启动 CDP 端口或未登录,① 的采集执行应禁用或在执行前汇总拦截并提示去④账号管理;可以提供跳转/启动登录入口,但不无提示批量启动所有账号 Chrome。
## workers 模块(`app/workers.py`,已建,PySide6) ## workers 模块(`app/workers.py`,已建,PySide6)
+6 -7
View File
@@ -6,9 +6,9 @@
## 当前快照 ## 当前快照
- 日期:2026-06-27 - 日期:2026-06-27
- 阶段:V0 单账号 CDP 流程已验证;V1 已完成 T-000 正式代码包结构、T-001 `app/editor.py` 模块化、T-002 `app/appconfig.py` 应用配置、T-003 SQLite 持久化地基、T-004 本地数据忽略规则、T-005 AI 模型清单后端、T-006 单元测试基座、T-101 账号 user-data-dir 工具、T-102 Chrome 启动器、T-103 登录保活与检测、T-104 PySide6 主窗口骨架、T-104b PySide6 worker 基座、T-105 Tab④ 账号管理、T-106 账号快捷方式、T-201 Excel 导入入库、T-202 Tab① 任务列表与导入按钮、T-202b Tab① 导入汇总栏、T-203 采集旧标题旧封面、T-204 回写旧字段到原 Excel。 - 阶段:V0 单账号 CDP 流程已验证;V1 已完成 T-000 正式代码包结构、T-001 `app/editor.py` 模块化、T-002 `app/appconfig.py` 应用配置、T-003 SQLite 持久化地基、T-004 本地数据忽略规则、T-005 AI 模型清单后端、T-006 单元测试基座、T-101 账号 user-data-dir 工具、T-102 Chrome 启动器、T-103 登录保活与检测、T-104 PySide6 主窗口骨架、T-104b PySide6 worker 基座、T-105 Tab④ 账号管理、T-106 账号快捷方式、T-201 Excel 导入入库、T-202 Tab① 任务列表与导入按钮、T-202b Tab① 导入汇总栏、T-203 采集旧标题旧封面、T-204 回写旧字段到原 Excel、T-204b 采集完成自动回写。
- 技术栈:Python 3.10+,自研 CDP(websocket-client + requests),SQLite(sqlite3)+ `config.json` + openpyxl + AI(服务商待定),GUI PySide6 5 Tab(已定)。 - 技术栈:Python 3.10+,自研 CDP(websocket-client + requests),SQLite(sqlite3)+ `config.json` + openpyxl + AI(服务商待定),GUI PySide6 5 Tab(已定)。
- 生产代码:已建立 `app/` 包 + 根入口 `main.py`;`app/cdp.py` 为已验证 CDP 底座;`app/editor.py` 已封装登录状态检测、标题/封面/采集/更新按钮能力;`app/appconfig.py` 已实现 `config.json` 默认值/读写/更新、AI 参数与端口读取,以及 `config/ai_models.json` 模型清单 CRUD/过滤/打码/测试连接;`app/db.py` 已实现 SQLite schema、连接 PRAGMA、批次/账号/任务与阶段写库函数;`app/excel.py` 已实现多 Excel 输入列解析、整文件列校验、脏行统计跳过、导入批次与任务入库、别名匹配统计、旧标题/旧封面路径回写原 Excel 与另存副本;`app/config.py` 已实现账号 slug 与 user-data-dir 创建;`app/accounts.py` 已实现账号 CRUD 服务、端口默认分配、启动登录、检测登录、生成快捷方式;`app/chrome.py` 已实现 Chrome 参数拼装、启动、CDP 端口探测、PowerShell `.lnk` 快捷方式生成;`app/gui.py` 已实现 PySide6 `MainWindow`、五 Tab、顶部 Tab 栏防误点样式、① 导入采集的 Excel 导入按钮/导入汇总栏/QTableView 任务列表/未匹配筛选与略过标记/采集旧标题旧封面 worker/旧数据回写按钮与 worker、④ 账号管理表格/弹窗/按钮/快捷方式与状态栏;`app/workers.py` 已实现 `BaseWorker`、通用 signals、取消标记和 `QThread` 启动包装。 - 生产代码:已建立 `app/` 包 + 根入口 `main.py`;`app/cdp.py` 为已验证 CDP 底座;`app/editor.py` 已封装登录状态检测、标题/封面/采集/更新按钮能力;`app/appconfig.py` 已实现 `config.json` 默认值/读写/更新、AI 参数与端口读取,以及 `config/ai_models.json` 模型清单 CRUD/过滤/打码/测试连接;`app/db.py` 已实现 SQLite schema、连接 PRAGMA、批次/账号/任务与阶段写库函数;`app/excel.py` 已实现多 Excel 输入列解析、整文件列校验、脏行统计跳过、导入批次与任务入库、别名匹配统计、旧标题/旧封面路径回写原 Excel 与另存副本;`app/config.py` 已实现账号 slug 与 user-data-dir 创建;`app/accounts.py` 已实现账号 CRUD 服务、端口默认分配、启动登录、检测登录、生成快捷方式;`app/chrome.py` 已实现 Chrome 参数拼装、启动、CDP 端口探测、PowerShell `.lnk` 快捷方式生成;`app/gui.py` 已实现 PySide6 `MainWindow`、五 Tab、顶部 Tab 栏防误点样式、① 导入采集的 Excel 导入按钮/导入汇总栏/QTableView 任务列表/未匹配筛选与略过标记/采集旧标题旧封面 worker/采集完成自动回写/旧数据回写重试按钮与 worker、④ 账号管理表格/弹窗/按钮/快捷方式与状态栏;`app/workers.py` 已实现 `BaseWorker`、通用 signals、取消标记和 `QThread` 启动包装。
- 测试:`tests/` 已建立;T-006 后纯逻辑改动必须运行 `python -m unittest discover -s tests`,当前覆盖 appconfig/db/config/accounts/chrome 启动与快捷方式/editor 登录检测/excel 导入/gui ① 导入采集/gui ④ 账号管理/worker signal 与线程包装,并对尚未实现的 app.prompts 做契约占位 skip;CDP/Shopee 改动仍需测试商品手动验证。 - 测试:`tests/` 已建立;T-006 后纯逻辑改动必须运行 `python -m unittest discover -s tests`,当前覆盖 appconfig/db/config/accounts/chrome 启动与快捷方式/editor 登录检测/excel 导入/gui ① 导入采集/gui ④ 账号管理/worker signal 与线程包装,并对尚未实现的 app.prompts 做契约占位 skip;CDP/Shopee 改动仍需测试商品手动验证。
- 数据:`config.json`、`config/ai_models.json`、`cmshopee.db`、`chrome_user_data_dir/`、`images/` 已由 `.gitignore` 排除;运营填写后的 Excel 业务文件默认忽略,标准空模板 `shopee待处理任务模板.xlsx` 可提交;`app/appconfig.py` 首次读取缺失的 `config.json` 时会在本地写默认配置,`app/db.py` 调用 `init_db()` 时会在本地创建 SQLite DB。 - 数据:`config.json`、`config/ai_models.json`、`cmshopee.db`、`chrome_user_data_dir/`、`images/` 已由 `.gitignore` 排除;运营填写后的 Excel 业务文件默认忽略,标准空模板 `shopee待处理任务模板.xlsx` 可提交;`app/appconfig.py` 首次读取缺失的 `config.json` 时会在本地写默认配置,`app/db.py` 调用 `init_db()` 时会在本地创建 SQLite DB。
@@ -32,7 +32,7 @@
| `prototypes/` | 已有 | 已验证原型/探查脚本(demo/set_title/set_cover/get_title/cookies/inspect_images/grab/1.py),保留作人工回归与探查参考;见 `prototypes/README.md` | | `prototypes/` | 已有 | 已验证原型/探查脚本(demo/set_title/set_cover/get_title/cookies/inspect_images/grab/1.py),保留作人工回归与探查参考;见 `prototypes/README.md` |
| `chrome-remote-debug-lan.md` | 已有 | WSL→Windows CDP 转发排查记录 | | `chrome-remote-debug-lan.md` | 已有 | WSL→Windows CDP 转发排查记录 |
| `app/__init__.py` / `app/__main__.py` / `main.py` | 已有 | 正式包与启动入口;`python main.py` / `python -m app` 可运行占位入口 | | `app/__init__.py` / `app/__main__.py` / `main.py` | 已有 | 正式包与启动入口;`python main.py` / `python -m app` 可运行占位入口 |
| `app/gui.py` | 已有 | T-104/T-105/T-106/T-202/T-202b/T-203/T-204 产出:PySide6 `QMainWindow` + 五 Tab;顶部 Tab 栏防误点样式;① 导入采集导入按钮、导入汇总栏、`QTableView` 任务列表、未匹配筛选与略过标记、采集旧标题旧封面 worker、旧数据回写 worker;④ 账号管理表格、账号弹窗、启动登录、检测登录、快捷方式 | | `app/gui.py` | 已有 | T-104/T-105/T-106/T-202/T-202b/T-203/T-204/T-204b 产出:PySide6 `QMainWindow` + 五 Tab;顶部 Tab 栏防误点样式;① 导入采集导入按钮、导入汇总栏、`QTableView` 任务列表、未匹配筛选与略过标记、采集旧标题旧封面 worker、采集完成自动回写与手动重试;④ 账号管理表格、账号弹窗、启动登录、检测登录、快捷方式 |
| `app/workers.py` | 已有 | T-104b 产出:`BaseWorker` + 通用 signals + 取消标记 + `run_worker()` QThread 包装 | | `app/workers.py` | 已有 | T-104b 产出:`BaseWorker` + 通用 signals + 取消标记 + `run_worker()` QThread 包装 |
| `app/accounts.py` | 已有 | T-105/T-106 产出:账号 CRUD 服务、目录创建、端口分配、启动登录、检测登录、快捷方式 | | `app/accounts.py` | 已有 | T-105/T-106 产出:账号 CRUD 服务、目录创建、端口分配、启动登录、检测登录、快捷方式 |
| `app/editor.py` | 已有 | T-001/T-103 产出:登录状态检测、打开商品页、读/写标题、读/下载封面、上传拖封面、更新按钮、apply_task | | `app/editor.py` | 已有 | T-001/T-103 产出:登录状态检测、打开商品页、读/写标题、读/下载封面、上传拖封面、更新按钮、apply_task |
@@ -40,7 +40,7 @@
| `app/db.py` | 已有 | T-003 产出:batches/accounts/tasks schema;WAL/busy_timeout/foreign_keys;账号/批次/任务与 set_* 阶段写库 | | `app/db.py` | 已有 | T-003 产出:batches/accounts/tasks schema;WAL/busy_timeout/foreign_keys;账号/批次/任务与 set_* 阶段写库 |
| `app/config.py` | 已有 | T-101 产出:别名→稳定 slug;创建并返回绝对 user-data-dir | | `app/config.py` | 已有 | T-101 产出:别名→稳定 slug;创建并返回绝对 user-data-dir |
| `app/chrome.py` | 已有 | T-102/T-106 产出:Chrome 启动参数、`subprocess.Popen` 启动、`/json/version` 端口探测、PowerShell `.lnk` 快捷方式 | | `app/chrome.py` | 已有 | T-102/T-106 产出:Chrome 启动参数、`subprocess.Popen` 启动、`/json/version` 端口探测、PowerShell `.lnk` 快捷方式 |
| `tests/` | 已有 | T-006/T-201/T-202/T-202b/T-203/T-204 产出:stdlib unittest 基座;覆盖 appconfig/db/config/accounts/chrome/editor/excel/gui/workers;prompts 模块契约占位测试 | | `tests/` | 已有 | T-006/T-201/T-202/T-202b/T-203/T-204/T-204b 产出:stdlib unittest 基座;覆盖 appconfig/db/config/accounts/chrome/editor/excel/gui/workers;prompts 模块契约占位测试 |
| `app/excel.py` | 已有 | T-201/T-204 产出:多文件 Excel 输入列解析、必需列整文件拒绝、脏行逐行跳过、批次/任务入库、匹配统计;按源文件/工作表/行号回写旧标题与旧封面路径;支持原文件被占用时另存副本 | | `app/excel.py` | 已有 | T-201/T-204 产出:多文件 Excel 输入列解析、必需列整文件拒绝、脏行逐行跳过、批次/任务入库、匹配统计;按源文件/工作表/行号回写旧标题与旧封面路径;支持原文件被占用时另存副本 |
| `shopee待处理任务模板.xlsx` | 已有,待提交 | 标准空 Excel 模板;单工作表 `待处理任务`,表头 `账号名 | 别名 | 商品id | 旧标题 | 旧封面图片路径 | 新标题 | 新封面图片路径 | 更新状态`;运营复制后填写,填写副本不提交 | | `shopee待处理任务模板.xlsx` | 已有,待提交 | 标准空 Excel 模板;单工作表 `待处理任务`,表头 `账号名 | 别名 | 商品id | 旧标题 | 旧封面图片路径 | 新标题 | 新封面图片路径 | 更新状态`;运营复制后填写,填写副本不提交 |
| `config.json` / `config/ai_models.json` / `cmshopee.db` / `chrome_user_data_dir/` / `images/` | 本地待建,已忽略 | 含配置、密钥、业务、登录态、图片,不提交版本库 | | `config.json` / `config/ai_models.json` / `cmshopee.db` / `chrome_user_data_dir/` / `images/` | 本地待建,已忽略 | 含配置、密钥、业务、登录态、图片,不提交版本库 |
@@ -57,13 +57,12 @@
任务状态以 [`06-tasks.md`](06-tasks.md) 为准,历史记录见 [`../progress.md`](../progress.md)。 任务状态以 [`06-tasks.md`](06-tasks.md) 为准,历史记录见 [`../progress.md`](../progress.md)。
- 已完成:T-000(正式代码包结构)、T-001(`app/editor.py` 模块化)、T-002(`app/appconfig.py` + `config.json`)、T-003(SQLite 建表)、T-004(本地数据 gitignore)、T-005(AI 模型清单后端)、T-006(单元测试基座)、T-101(账号 slug/user-data-dir)、T-102(Chrome 启动器)、T-103(登录保活与检测)、T-104(PySide6 五 Tab 主窗口骨架)、T-104b(PySide6 worker 基座)、T-105(Tab④ 账号管理)、T-106(账号快捷方式)、T-201(Excel 导入:解析多文件输入列入库)、T-202(Tab① 任务列表 + 导入按钮 + 别名匹配标记)、T-202b(Tab① 导入汇总栏)、T-203(采集旧标题+旧封面)、T-204(回写旧字段到原 Excel)。 - 已完成:T-000(正式代码包结构)、T-001(`app/editor.py` 模块化)、T-002(`app/appconfig.py` + `config.json`)、T-003(SQLite 建表)、T-004(本地数据 gitignore)、T-005(AI 模型清单后端)、T-006(单元测试基座)、T-101(账号 slug/user-data-dir)、T-102(Chrome 启动器)、T-103(登录保活与检测)、T-104(PySide6 五 Tab 主窗口骨架)、T-104b(PySide6 worker 基座)、T-105(Tab④ 账号管理)、T-106(账号快捷方式)、T-201(Excel 导入:解析多文件输入列入库)、T-202(Tab① 任务列表 + 导入按钮 + 别名匹配标记)、T-202b(Tab① 导入汇总栏)、T-203(采集旧标题+旧封面)、T-204(回写旧字段到原 Excel)、T-204b(采集完成自动回写旧字段)。
- 正在进行:无。 - 正在进行:无。
- 下一个可领取任务:**T-204b(采集完成后自动回写旧字段到 Excel)**。 - 下一个可领取任务:**T-205(首次未配账号 / Chrome 未启动 / 未登录的引导保护)**。
## 当前已发现待修体验问题 ## 当前已发现待修体验问题
- T-204 已提供手动「回写旧数据到 Excel」,但当前采集完成后不会自动写回原 Excel;已拆为 T-204b 修复,目标是采集完成自动回写,失败时保留手动重试。
- ① 采集依赖对应账号 Chrome 已用专属 user-data-dir 和 CDP 端口启动并登录;若未启动或未登录,当前会采集失败/略过。已在 T-205 明确处理:禁用或提示,并引导去④账号管理启动登录,不无提示批量启动所有账号 Chrome。 - ① 采集依赖对应账号 Chrome 已用专属 user-data-dir 和 CDP 端口启动并登录;若未启动或未登录,当前会采集失败/略过。已在 T-205 明确处理:禁用或提示,并引导去④账号管理启动登录,不无提示批量启动所有账号 Chrome。
## 当前可运行内容 ## 当前可运行内容
+8
View File
@@ -434,3 +434,11 @@
- 变更:`docs/06-tasks.md` 新增 T-204b(采集完成后自动回写旧字段到 Excel),并把 T-205 扩展为“未配账号 / Chrome 未启动 / 未登录”的引导保护;同步 `docs/current-state.md` 下一个可领取任务为 T-204b,并补充当前已发现待修体验问题;同步 `docs/routes.md`、`docs/api.md`、`docs/04-architecture.md`、`docs/02-requirements.md`。 - 变更:`docs/06-tasks.md` 新增 T-204b(采集完成后自动回写旧字段到 Excel),并把 T-205 扩展为“未配账号 / Chrome 未启动 / 未登录”的引导保护;同步 `docs/current-state.md` 下一个可领取任务为 T-204b,并补充当前已发现待修体验问题;同步 `docs/routes.md`、`docs/api.md`、`docs/04-architecture.md`、`docs/02-requirements.md`。
- 决策:T-204b 先解决采集闭环自动回写,保留手动回写作为锁文件失败后的重试入口;T-205 解决 Chrome 未启动/未登录的禁用、提示和跳转④,不做无提示批量启动所有账号 Chrome。 - 决策:T-204b 先解决采集闭环自动回写,保留手动回写作为锁文件失败后的重试入口;T-205 解决 Chrome 未启动/未登录的禁用、提示和跳转④,不做无提示批量启动所有账号 Chrome。
- 验证:文档-only 更新,未运行单元测试。 - 验证:文档-only 更新,未运行单元测试。
## 【2026-06-27】T-204b 采集完成自动回写旧字段
- 状态:DONE
- 变更:`app/gui.py` 将 Tab① 手动回写启动逻辑抽为 `_start_write_back()`,`CollectTab._on_collect_finished()` 在本轮成功采集数量大于 0 时自动启动 `WriteBackWorker` 回写当前批次旧标题/旧封面路径;自动回写成功时状态栏显示“Excel 自动回写完成”;自动回写遇到原 Excel 被占用时弹窗提示关闭文件后点击「回写旧数据到 Excel」手动重试,SQLite 采集结果不回滚。更新 `tests/test_gui.py` 覆盖自动触发、无成功采集不触发、锁文件提示手动重试;同步 `docs/06-tasks.md`、`docs/current-state.md`、`docs/api.md`、`docs/04-architecture.md`。
- 细节:手动「回写旧数据到 Excel」按钮保留,主要作为自动回写失败后的重试入口;若回写 worker 已在运行,会提示“Excel 回写正在进行”,避免重复启动。
- 验证:`python -m unittest discover -s tests -p "test_gui.py"` 通过(14 tests);`python -m compileall app main.py tests` 通过;`python -m unittest discover -s tests` 通过(56 tests,skipped=1);`py -3 -m compileall app main.py tests` 通过;`py -3 -m unittest discover -s tests` 通过(34 tests,skipped=4,py -3 环境缺 openpyxl/PySide6,相关测试按设计跳过)。
- 下一步:按任务看板领取 T-205(首次未配账号 / Chrome 未启动 / 未登录的引导保护)。
+66
View File
@@ -387,6 +387,72 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir) self.assert_removed(temp_dir)
def test_collect_tab_auto_starts_write_back_after_collect_success(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
db.init_db(cfg["db_path"])
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"],
)
statuses = []
tab = CollectTab(config=cfg, status_callback=statuses.append)
self.addCleanup(tab.close)
with mock.patch.object(tab, "_start_write_back", return_value=True) as start_write_back:
tab._on_collect_finished({"collected": 1, "skipped": 0, "failed": 0})
start_write_back.assert_called_once_with(batch_id, auto=True)
self.assertIn("正在自动回写 Excel", statuses[-1])
self.assert_removed(temp_dir)
def test_collect_tab_does_not_auto_write_back_when_nothing_collected(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
statuses = []
tab = CollectTab(config=cfg, status_callback=statuses.append)
self.addCleanup(tab.close)
with mock.patch.object(tab, "_start_write_back") as start_write_back:
tab._on_collect_finished({"collected": 0, "skipped": 1, "failed": 0})
start_write_back.assert_not_called()
self.assertEqual("采集完成:成功0,略过1,失败0", statuses[-1])
self.assert_removed(temp_dir)
def test_auto_write_back_locked_file_message_points_to_manual_retry(self):
with self.make_temp_dir() as temp_dir:
statuses = []
tab = CollectTab(config=self.make_config(temp_dir), status_callback=statuses.append)
self.addCleanup(tab.close)
with mock.patch("app.gui.QMessageBox.warning") as warning:
tab._on_write_back_failed(
-1,
"Excel 文件被占用,请关闭后重试: input.xlsx",
auto=True,
)
message = warning.call_args[0][2]
self.assertIn("Excel 自动回写失败", message)
self.assertIn("点击「回写旧数据到 Excel」手动重试", message)
self.assertIn("手动重试", statuses[-1])
self.assert_removed(temp_dir)
def test_write_back_worker_calls_excel_write_back(self): def test_write_back_worker_calls_excel_write_back(self):
with mock.patch( with mock.patch(
"app.gui.excel.write_back", "app.gui.excel.write_back",