feat: localize Shopee GUI wording

This commit is contained in:
chengma
2026-07-07 14:58:54 +08:00
parent 7838554c3d
commit 862f791fde
25 changed files with 163 additions and 136 deletions
+3 -3
View File
@@ -615,9 +615,9 @@ def _cover_upload_error_message(result):
if reason == "UPLOAD_CROP_REQUIRED": if reason == "UPLOAD_CROP_REQUIRED":
return "新封面上传后出现裁剪确认框,当前版本未自动处理裁剪弹窗" return "新封面上传后出现裁剪确认框,当前版本未自动处理裁剪弹窗"
if reason == "UPLOAD_STILL_PROCESSING": if reason == "UPLOAD_STILL_PROCESSING":
return "新封面上传仍在处理中,未取得 Shopee CDN 地址" return "新封面上传仍在处理中,未取得蝦皮 CDN 地址"
if reason == "UPLOAD_TIMEOUT": if reason == "UPLOAD_TIMEOUT":
return "新封面上传超时,未取得 Shopee CDN 地址" return "新封面上传超时,未取得蝦皮 CDN 地址"
if reason == "UPLOAD_TILE_NOT_READY": if reason == "UPLOAD_TILE_NOT_READY":
return "新封面上传入口未可点击,未开始上传新封面" return "新封面上传入口未可点击,未开始上传新封面"
if reason == "IMAGE_MANAGER_BUSY": if reason == "IMAGE_MANAGER_BUSY":
@@ -648,7 +648,7 @@ def _wait_ready(cdp, timeout=60):
if invalid_text: if invalid_text:
raise EditorError(product_unavailable_error_message(invalid_text)) raise EditorError(product_unavailable_error_message(invalid_text))
time.sleep(1) time.sleep(1)
raise EditorError(_open_product_ready_error(cdp, "等待 Shopee 商品编辑器就绪超时")) raise EditorError(_open_product_ready_error(cdp, "等待蝦皮商品编辑器就绪超时"))
def _ensure_page_domains(cdp): def _ensure_page_domains(cdp):
+2 -2
View File
@@ -101,7 +101,7 @@ class MainWindow(QMainWindow):
self.tabs.currentChanged.connect(self._on_tab_changed) self.tabs.currentChanged.connect(self._on_tab_changed)
for title in TAB_TITLES: for title in TAB_TITLES:
self.tabs.addTab(self._build_tab(title), title) self.tabs.addTab(self._build_tab(title), title)
self.tabs.setTabIcon(TAB_TITLES.index("③ 更新shopee"), _warning_dot_icon()) self.tabs.setTabIcon(TAB_TITLES.index("③ 更新蝦皮"), _warning_dot_icon())
self.setCentralWidget(self.tabs) self.setCentralWidget(self.tabs)
self.statusBar().showMessage("就绪") self.statusBar().showMessage("就绪")
@@ -130,7 +130,7 @@ class MainWindow(QMainWindow):
cover_prompts_dir=appconfig.cover_prompts_dir(self.config), cover_prompts_dir=appconfig.cover_prompts_dir(self.config),
open_accounts_callback=lambda: self.open_accounts_tab(), open_accounts_callback=lambda: self.open_accounts_tab(),
) )
if title == "③ 更新shopee": if title == "③ 更新蝦皮":
return ApplyTab( return ApplyTab(
db_path=self.db_path, db_path=self.db_path,
config=self.config, config=self.config,
+10 -11
View File
@@ -194,7 +194,7 @@ class ApplyTab(QWidget):
self.empty_state_card, self.empty_state_card,
self.empty_state_label, self.empty_state_label,
self.empty_state_button, self.empty_state_button,
"第一步:前往『④账号管理』配置并登录账号,再回到③更新 Shopee。", "第一步:前往『④账号管理』配置并登录账号,再回到③更新蝦皮。",
self.open_accounts_callback is not None, self.open_accounts_callback is not None,
) )
return return
@@ -327,12 +327,12 @@ class ApplyTab(QWidget):
f"店铺:{self.model.account_name_for(task)}", f"店铺:{self.model.account_name_for(task)}",
"", "",
"将保留新标题和新封面路径,只把本地状态退回可更新。", "将保留新标题和新封面路径,只把本地状态退回可更新。",
"不会触碰 Shopee,也不会自动回写 Excel。", "不会触碰蝦皮,也不会自动回写 Excel。",
] ]
if getattr(task, "committed", 0): if getattr(task, "committed", 0):
lines.extend([ lines.extend([
"", "",
"注意:该记录已经提交过线上。本地重置不会回滚 Shopee,重复更新会再次提交线上。", "注意:该记录已经提交过线上。本地重置不会回滚蝦皮,重复更新会再次提交线上。",
]) ])
answer = QMessageBox.question( answer = QMessageBox.question(
self, self,
@@ -471,7 +471,7 @@ class ApplyTab(QWidget):
intro = ( intro = (
"即将检查当前筛选结果。\n\n" "即将检查当前筛选结果。\n\n"
if dry_run if dry_run
else "即将按当前筛选结果分批更新 Shopee 线上商品。\n\n" else "即将按当前筛选结果分批更新蝦皮线上商品。\n\n"
) )
return ( return (
intro intro
@@ -487,7 +487,7 @@ class ApplyTab(QWidget):
+ f"成功后关闭新页={close_text}," + f"成功后关闭新页={close_text},"
+ f"多账号并行={parallel_text}\n\n" + f"多账号并行={parallel_text}\n\n"
+ ( + (
"检查只写运行日志,不打开 Shopee、不点击「更新」、不改任务状态。" "检查只写运行日志,不打开蝦皮、不点击「更新」、不改任务状态。"
if dry_run if dry_run
else f"确认后会打开商品编辑页、替换标题/允许时替换封面,并按每批最多 {batch_size} 条点击「更新」提交线上;点击停止后不再开始下一条或下一批。" else f"确认后会打开商品编辑页、替换标题/允许时替换封面,并按每批最多 {batch_size} 条点击「更新」提交线上;点击停止后不再开始下一条或下一批。"
) )
@@ -500,7 +500,7 @@ class ApplyTab(QWidget):
if not update_cfg.get("allow_real_submit", False): if not update_cfg.get("allow_real_submit", False):
return ( return (
"设置未开启「允许真实提交线上商品」,已阻止本次更新。\n" "设置未开启「允许真实提交线上商品」,已阻止本次更新。\n"
"请到⑤设置 > Shopee 更新安全开启该开关后再开始更新。" "请到⑤设置 > 蝦皮更新安全开启该开关后再开始更新。"
) )
if not update_cfg.get("allow_cover_update", False): if not update_cfg.get("allow_cover_update", False):
cover_tasks = [ cover_tasks = [
@@ -511,7 +511,7 @@ class ApplyTab(QWidget):
if cover_tasks: if cover_tasks:
return ( return (
"设置未开启「允许更新封面」,当前任务包含新封面路径,已阻止本次更新。\n" "设置未开启「允许更新封面」,当前任务包含新封面路径,已阻止本次更新。\n"
"请到⑤设置 > Shopee 更新安全开启该开关,或先筛掉含新封面的任务。" "请到⑤设置 > 蝦皮更新安全开启该开关,或先筛掉含新封面的任务。"
) )
return None return None
@@ -674,7 +674,7 @@ class ApplyTab(QWidget):
logged_out = payload.get("logged_out") or [] logged_out = payload.get("logged_out") or []
if logged_out: if logged_out:
lines.append( lines.append(
"以下账号未登录 Shopee:" "以下账号未登录蝦皮:"
+ "、".join(self._account_label(item) for item in logged_out) + "、".join(self._account_label(item) for item in logged_out)
) )
self._show_account_guide("\n".join(lines)) self._show_account_guide("\n".join(lines))
@@ -683,7 +683,7 @@ class ApplyTab(QWidget):
full_message = ( full_message = (
f"{message}\n\n" f"{message}\n\n"
"本轮更新已中止,不会自动打开账号 Chrome,也不会提交任何商品。\n" "本轮更新已中止,不会自动打开账号 Chrome,也不会提交任何商品。\n"
"请先到「④ 账号管理」配置账号、启动对应账号 Chrome,并确认已人工登录 Shopee。" "请先到「④ 账号管理」配置账号、启动对应账号 Chrome,并确认已人工登录蝦皮。"
) )
QMessageBox.warning(self, "账号未就绪", full_message) QMessageBox.warning(self, "账号未就绪", full_message)
self._set_status(full_message.replace("\n", " ")) self._set_status(full_message.replace("\n", " "))
@@ -805,7 +805,7 @@ class ApplyTab(QWidget):
def _apply_summary_message(self, apply_summary, write_back_payload=None, error=None): def _apply_summary_message(self, apply_summary, write_back_payload=None, error=None):
dry_run = bool(apply_summary.get("dry_run")) dry_run = bool(apply_summary.get("dry_run"))
lines = [ lines = [
"检查本轮更新完成,未打开 Shopee、未提交线上、未改任务状态。" "检查本轮更新完成,未打开蝦皮、未提交线上、未改任务状态。"
if dry_run if dry_run
else "更新完成。", else "更新完成。",
"{success_label}:{applied},失败:{failed},略过:{skipped}".format( "{success_label}:{applied},失败:{failed},略过:{skipped}".format(
@@ -825,4 +825,3 @@ class ApplyTab(QWidget):
if error: if error:
lines.append(str(error)) lines.append(str(error))
return "\n".join(lines) return "\n".join(lines)
+3 -4
View File
@@ -202,7 +202,7 @@ class CollectTab(QWidget):
full_message = ( full_message = (
f"{message}\n\n" f"{message}\n\n"
"本轮采集已中止,不会自动打开账号 Chrome。\n" "本轮采集已中止,不会自动打开账号 Chrome。\n"
"请先到「④ 账号管理」配置账号、启动对应账号 Chrome,并确认已人工登录 Shopee。" "请先到「④ 账号管理」配置账号、启动对应账号 Chrome,并确认已人工登录蝦皮。"
) )
QMessageBox.warning(self, "账号未就绪", full_message) QMessageBox.warning(self, "账号未就绪", full_message)
self._set_status(full_message.replace("\n", " ")) self._set_status(full_message.replace("\n", " "))
@@ -470,7 +470,7 @@ class CollectTab(QWidget):
f"已提交线上:{committed_count}", f"已提交线上:{committed_count}",
"", "",
"软删除后,该批次不会再出现在①/②/③页面、筛选、采集、生成、更新或回写入口中。", "软删除后,该批次不会再出现在①/②/③页面、筛选、采集、生成、更新或回写入口中。",
"软删除只隐藏本地批次,不会回滚 Shopee 线上修改,不删除原始 Excel,也不删除本地图片。", "软删除只隐藏本地批次,不会回滚蝦皮线上修改,不删除原始 Excel,也不删除本地图片。",
] ]
answer = QMessageBox.question( answer = QMessageBox.question(
self, self,
@@ -673,7 +673,7 @@ class CollectTab(QWidget):
logged_out = payload.get("logged_out") or [] logged_out = payload.get("logged_out") or []
if logged_out: if logged_out:
lines.append( lines.append(
"以下账号未登录 Shopee:" "以下账号未登录蝦皮:"
+ "、".join(self._account_label(item) for item in logged_out) + "、".join(self._account_label(item) for item in logged_out)
) )
self._show_account_guide("\n".join(lines)) self._show_account_guide("\n".join(lines))
@@ -837,4 +837,3 @@ class CollectTab(QWidget):
"" if unmatched == 0 else f"{unmatched} 条任务别名未匹配账号,阶段显示为“略过”" "" if unmatched == 0 else f"{unmatched} 条任务别名未匹配账号,阶段显示为“略过”"
) )
+2 -2
View File
@@ -624,13 +624,13 @@ class GenerateTab(QWidget):
"重置封面:只清空新封面,保留新标题和手动微调标题。", "重置封面:只清空新封面,保留新标题和手动微调标题。",
"重置全部:清空新标题和新封面。", "重置全部:清空新标题和新封面。",
"", "",
"不会触碰 Shopee,也不会自动回写 Excel;后续重新生成会消耗 cmhub 点数。", "不会触碰蝦皮,也不会自动回写 Excel;后续重新生成会消耗 cmhub 点数。",
"默认不删除本地新封面文件。", "默认不删除本地新封面文件。",
]) ])
if committed_count: if committed_count:
lines.extend([ lines.extend([
"", "",
f"注意:其中 {committed_count} 条已经提交过线上。本地重置不会回滚 Shopee,重生成后再更新会再次提交线上。", f"注意:其中 {committed_count} 条已经提交过线上。本地重置不会回滚蝦皮,重生成后再更新会再次提交线上。",
]) ])
box = QMessageBox(self) box = QMessageBox(self)
box.setWindowTitle("重置生成结果") box.setWindowTitle("重置生成结果")
+1 -1
View File
@@ -336,7 +336,7 @@ class SettingsTab(QWidget):
"settingsGenerationSectionTitle", "settingsGenerationSectionTitle",
) )
self.shopee_update_section_title = self._section_title( self.shopee_update_section_title = self._section_title(
"Shopee 更新安全 / 执行模式", "蝦皮更新安全 / 执行模式",
"settingsShopeeUpdateSectionTitle", "settingsShopeeUpdateSectionTitle",
) )
self.infrastructure_section_title = self._section_title( self.infrastructure_section_title = self._section_title(
+1 -1
View File
@@ -55,7 +55,7 @@ except ModuleNotFoundError as exc:
TAB_TITLES = [ TAB_TITLES = [
"① 导入采集", "① 导入采集",
"② AI生成", "② AI生成",
"③ 更新shopee", "③ 更新蝦皮",
"④ 账号管理", "④ 账号管理",
"⑤ 设置", "⑤ 设置",
] ]
+4 -4
View File
@@ -806,7 +806,7 @@ class ApplyWorker(BaseWorker):
task=task, task=task,
) )
self._write_diagnostic_log( self._write_diagnostic_log(
"Shopee更新任务失败", "蝦皮更新任务失败",
level="ERROR", level="ERROR",
step=failed_step, step=failed_step,
task=task, task=task,
@@ -829,7 +829,7 @@ class ApplyWorker(BaseWorker):
level="error", level="error",
) )
self._write_diagnostic_log( self._write_diagnostic_log(
"Shopee更新任务异常", "蝦皮更新任务异常",
level="ERROR", level="ERROR",
step=current_step, step=current_step,
task=task, task=task,
@@ -1001,7 +1001,7 @@ class ApplyWorker(BaseWorker):
) )
for item in blocked.get("logged_out") or []: for item in blocked.get("logged_out") or []:
self._log_run_event( self._log_run_event(
"step=login_check result=blocked detail=账号 {alias} 未登录 Shopee: {reason}".format( "step=login_check result=blocked detail=账号 {alias} 未登录蝦皮: {reason}".format(
alias=item.get("alias") or "", alias=item.get("alias") or "",
reason=item.get("reason") or "", reason=item.get("reason") or "",
), ),
@@ -1460,7 +1460,7 @@ class CollectWorker(BaseWorker):
) )
for item in blocked.get("logged_out") or []: for item in blocked.get("logged_out") or []:
self._log_run_event( self._log_run_event(
"step=preflight result=blocked detail=账号 {alias} 未登录 Shopee: {reason}".format( "step=preflight result=blocked detail=账号 {alias} 未登录蝦皮: {reason}".format(
alias=item.get("alias") or "", alias=item.get("alias") or "",
reason=item.get("reason") or "", reason=item.get("reason") or "",
), ),
+3 -3
View File
@@ -7,7 +7,7 @@
蝦皮圈優化助手(代号 cmshopee)是一个 Windows 本地桌面自动化工具(PySide6,5 Tab),让运营管理多个 Shopee 账号,并用 CDP 驱动 Chrome + AI 批量改商品标题、换商品封面。 蝦皮圈優化助手(代号 cmshopee)是一个 Windows 本地桌面自动化工具(PySide6,5 Tab),让运营管理多个 Shopee 账号,并用 CDP 驱动 Chrome + AI 批量改商品标题、换商品封面。
5 Tab 流水线(工作流优先顺序): 5 Tab 流水线(工作流优先顺序):
**① 导入采集 → ② AI生成 → ③ 更新shopee → ④ 账号管理 → ⑤ 设置** **① 导入采集 → ② AI生成 → ③ 更新蝦皮 → ④ 账号管理 → ⑤ 设置**
目标闭环:④ 配账号并登录 → ① 导入 Excel(按“别名”列关联账号)、采集旧标题/旧封面并回写 → ② 用提示词 AI 生成新标题/新封面(不设逐条确认阶段)→ ③ 对已生成任务点击「开始更新」,弹窗确认后批量改标题+换封面并点「更新」提交 → 结果实时存 SQLite、批量回写原 Excel。 目标闭环:④ 配账号并登录 → ① 导入 Excel(按“别名”列关联账号)、采集旧标题/旧封面并回写 → ② 用提示词 AI 生成新标题/新封面(不设逐条确认阶段)→ ③ 对已生成任务点击「开始更新」,弹窗确认后批量改标题+换封面并点「更新」提交 → 结果实时存 SQLite、批量回写原 Excel。
@@ -38,7 +38,7 @@
2. Phase 1:账号绑定 user-data-dir、Chrome 启动、登录保活、PySide6 主窗口骨架。 2. Phase 1:账号绑定 user-data-dir、Chrome 启动、登录保活、PySide6 主窗口骨架。
3. Phase 2:导入 Excel、采集旧标题/旧封面并回写。 3. Phase 2:导入 Excel、采集旧标题/旧封面并回写。
4. Phase 3:AI 生成新标题/新封面。 4. Phase 3:AI 生成新标题/新封面。
5. Phase 4:③ 批量确认后更新 Shopee 并回写结果。 5. Phase 4:③ 批量确认后更新蝦皮并回写结果。
6. Phase 5:设置、敏感信息提示与日志脱敏、收尾。 6. Phase 5:设置、敏感信息提示与日志脱敏、收尾。
## 领取任务规则 ## 领取任务规则
@@ -65,7 +65,7 @@
**V1 当前 coding 目标**: **V1 当前 coding 目标**:
- 5 Tab 流水线:① 导入采集 → ② AI生成 → ③ 更新shopee → ④ 账号管理 → ⑤ 设置。 - 5 Tab 流水线:① 导入采集 → ② AI生成 → ③ 更新蝦皮 → ④ 账号管理 → ⑤ 设置。
- GUI 固定为 PySide6;后台采集/生成/更新用 `QObject` worker + `QThread` + signal 回传进度。 - GUI 固定为 PySide6;后台采集/生成/更新用 `QObject` worker + `QThread` + signal 回传进度。
- 多账号管理;账号以独立 user-data-dir 隔离。③ 更新默认串行,提供「检查本轮更新」按钮;⑤ 可开启按账号并行和设置每批最大更新条数。 - 多账号管理;账号以独立 user-data-dir 隔离。③ 更新默认串行,提供「检查本轮更新」按钮;⑤ 可开启按账号并行和设置每批最大更新条数。
- Excel 导入/回写 + SQLite 实时落库 + 本地图片目录。 - Excel 导入/回写 + SQLite 实时落库 + 本地图片目录。
+2 -2
View File
@@ -34,7 +34,7 @@
### V1 当前目标(多账号 + Excel + AI,5 Tab 流水线) ### V1 当前目标(多账号 + Excel + AI,5 Tab 流水线)
5 个 Tab,顺序:① 导入采集 → ② AI生成 → ③ 更新shopee → ④ 账号管理 → ⑤ 设置。 5 个 Tab,顺序:① 导入采集 → ② AI生成 → ③ 更新蝦皮 → ④ 账号管理 → ⑤ 设置。
| 功能 | 用户能做什么 | 优先级 | | 功能 | 用户能做什么 | 优先级 |
| --- | --- | --- | | --- | --- | --- |
@@ -45,7 +45,7 @@
| AI 生成(②) | 输入标题/封面提示词,AI 据提示词+旧标题生成新标题;封面图片按②本轮「生成封面图片(成本较高)」开关可选生成,避免用户无意产生图片模型成本;点击「开始生成」时清空②界面旧日志,只显示本轮生成日志,历史 run_logs 仍保留 | P0 | | AI 生成(②) | 输入标题/封面提示词,AI 据提示词+旧标题生成新标题;封面图片按②本轮「生成封面图片(成本较高)」开关可选生成,避免用户无意产生图片模型成本;点击「开始生成」时清空②界面旧日志,只显示本轮生成日志,历史 run_logs 仍保留 | P0 |
| 提示词管理(②) | 标题提示词「保存」到 `data/title_prompt.txt` 并启动回显;封面提示词多模板(下拉 + 新建/保存/另存为/重命名/删除)+ 插入 `{新标题}` + 预览(变量替换) | P0 | | 提示词管理(②) | 标题提示词「保存」到 `data/title_prompt.txt` 并启动回显;封面提示词多模板(下拉 + 新建/保存/另存为/重命名/删除)+ 插入 `{新标题}` + 预览(变量替换) | P0 |
| 查看对照(②) | 双击任务弹窗查看新旧封面(纯查看,无逐条审核阶段);可选对单行重生成 | P0 | | 查看对照(②) | 双击任务弹窗查看新旧封面(纯查看,无逐条审核阶段);可选对单行重生成 | P0 |
| 更新 shopee(③) | 按批次/店铺/状态筛选;可先「检查本轮更新」确认范围;点击「开始更新」后弹窗确认,确认后对当前筛选出的已生成任务按每批最大条数分批更新 Shopee,逐条换标题+封面并点「更新」提交线上;普通正式更新不再受测试商品 ID 限制,可批量更新真实商品;可按状态=失败重试 | P0 | | 更新蝦皮(③) | 按批次/店铺/状态筛选;可先「检查本轮更新」确认范围;点击「开始更新」后弹窗确认,确认后对当前筛选出的已生成任务按每批最大条数分批更新蝦皮商品,逐条换标题+封面并点「更新」提交线上;普通正式更新不再受测试商品 ID 限制,可批量更新真实商品;可按状态=失败重试 | P0 |
| 检查本轮更新 / 运行日志 / 多账号并行(③/⑤) | ③ 提供「检查本轮更新」按钮,不打开 Shopee、不提交、不改任务状态,只显示当前筛选范围、店铺分布、预计分批和会更新字段;点击「检查本轮更新」或「开始更新」时清空③界面旧日志,只显示本轮检查/更新日志;真实更新写运行日志;可在⑤开启多账号并行,同账号内仍串行 | V2 已接入,检查按钮已接入 | | 检查本轮更新 / 运行日志 / 多账号并行(③/⑤) | ③ 提供「检查本轮更新」按钮,不打开 Shopee、不提交、不改任务状态,只显示当前筛选范围、店铺分布、预计分批和会更新字段;点击「检查本轮更新」或「开始更新」时清空③界面旧日志,只显示本轮检查/更新日志;真实更新写运行日志;可在⑤开启多账号并行,同账号内仍串行 | V2 已接入,检查按钮已接入 |
| 结果存储与回写 | 各阶段结果实时存 SQLite;该文件全部完成后把旧/新数据+状态批量回写原 Excel | P0 | | 结果存储与回写 | 各阶段结果实时存 SQLite;该文件全部完成后把旧/新数据+状态批量回写原 Excel | P0 |
| 设置(⑤) | AI 模型管理(下拉+新增/删除/详情/测试连接,至少各一个文本+图像模型);标题/图片大模型角色选择;分辨率(512/1k/2k/4k,返回超时随分辨率自动);并发/重试/jpg质量;图片目录/Chrome 路径/端口;设置页采用居中内容区、适度左右留白和响应式三列表单,长字段跨列显示;点击「保存设置」成功后弹轻量提示框 | P0 | | 设置(⑤) | AI 模型管理(下拉+新增/删除/详情/测试连接,至少各一个文本+图像模型);标题/图片大模型角色选择;分辨率(512/1k/2k/4k,返回超时随分辨率自动);并发/重试/jpg质量;图片目录/Chrome 路径/端口;设置页采用居中内容区、适度左右留白和响应式三列表单,长字段跨列显示;点击「保存设置」成功后弹轻量提示框 | P0 |
+4 -4
View File
@@ -12,7 +12,7 @@ Windows 本地桌面自动化工具,无后端服务,5 Tab GUI 驱动一条
| |
v v
GUI(PySide6 QTabWidget,5 Tab) GUI(PySide6 QTabWidget,5 Tab)
① 导入采集 ② AI生成 ③ 更新shopee ④ 账号管理 ⑤ 设置 ① 导入采集 ② AI生成 ③ 更新蝦皮 ④ 账号管理 ⑤ 设置
| |
v v
核心模块(Python) 核心模块(Python)
@@ -318,7 +318,7 @@ CREATE TABLE run_log_events (
1. **T-207 已接入 ① 采集**:记录批次开始/结束、账号预检、每个商品开始/成功/失败/略过;关键步骤覆盖 `preflight`、`open_product`、`wait_ready`、`read_title`、`read_cover`、`download_cover`、`db_write`、`excel_write_back`。失败时 DB 事件保存最后步骤和简短错误,本地 `data/logs/cmshopee.log` 保存完整脱敏 traceback。 1. **T-207 已接入 ① 采集**:记录批次开始/结束、账号预检、每个商品开始/成功/失败/略过;关键步骤覆盖 `preflight`、`open_product`、`wait_ready`、`read_title`、`read_cover`、`download_cover`、`db_write`、`excel_write_back`。失败时 DB 事件保存最后步骤和简短错误,本地 `data/logs/cmshopee.log` 保存完整脱敏 traceback。
2. **② AI生成已先补诊断**:`GenerateWorker` 创建 `run_type=generate`,按任务记录标题/封面阶段事件;关键步骤覆盖 `title_submit`、`load_text_model`、`title_build_request`、`title_request`、`title_parse_response`、`cover_prompt_render`、`cover_submit`、`cover_validate_input`、`load_image_model`、`cover_build_request`、`cover_request`、`cover_parse_response`、`cover_save`、`db_write`。失败时任务 `last_error`、DB 运行日志和本地 `data/logs/cmshopee.log` 都写脱敏错误。 2. **② AI生成已先补诊断**:`GenerateWorker` 创建 `run_type=generate`,按任务记录标题/封面阶段事件;关键步骤覆盖 `title_submit`、`load_text_model`、`title_build_request`、`title_request`、`title_parse_response`、`cover_prompt_render`、`cover_submit`、`cover_validate_input`、`load_image_model`、`cover_build_request`、`cover_request`、`cover_parse_response`、`cover_save`、`db_write`。失败时任务 `last_error`、DB 运行日志和本地 `data/logs/cmshopee.log` 都写脱敏错误。
3. **T-505 已扩展全流程**:Excel 导入创建 `run_type=import`,记录选中文件、缺列、脏行、入库统计;Excel 回写创建 `run_type=write_back`,记录文件写入、文件锁/保存异常和行数;③ 更新shopee 的 `run_type=apply` 覆盖安全/账号预检、Chrome/CDP 检查、登录检测、打开商品页、改标题、换封面、点更新、写库;④「启动登录」创建 `run_type=chrome_launch`,④「检测登录」创建 `run_type=login_check`;⑤ AI 模型测试连接创建 `run_type=ai_model_test`。失败时本地 `data/logs/cmshopee.log` 写脱敏 traceback,业务日志和状态栏只写脱敏短错误。 3. **T-505 已扩展全流程**:Excel 导入创建 `run_type=import`,记录选中文件、缺列、脏行、入库统计;Excel 回写创建 `run_type=write_back`,记录文件写入、文件锁/保存异常和行数;③ 更新蝦皮 的 `run_type=apply` 覆盖安全/账号预检、Chrome/CDP 检查、登录检测、打开商品页、改标题、换封面、点更新、写库;④「启动登录」创建 `run_type=chrome_launch`,④「检测登录」创建 `run_type=login_check`;⑤ AI 模型测试连接创建 `run_type=ai_model_test`。失败时本地 `data/logs/cmshopee.log` 写脱敏 traceback,业务日志和状态栏只写脱敏短错误。
4. **T-404b 已接入商品页失败 toast 捕获**:①采集和③更新在 `open_product`/等待详情页就绪失败时,不应只报等待超时。进入/刷新商品编辑页后应捕获 `.eds-toasts .eds-toast__content` 的文本和 `outerHTML`,记录当前 URL、时间、可见状态,并在关键元素超时时把最近错误 toast 作为用户可读失败原因写入 `last_error`、`run_log_events` 与本地诊断日志;例如商品 ID 失效时提示 `please input correct product id`。若 toast 明确属于商品失效/商品不存在/无权限类错误,底层仍写 `stage=imported/status=failed/last_error=商品失效:<原始toast>`,只在①导入采集列表“阶段”显示“商品失效”;其他打开失败仍显示“失败”。若失败发生在 `open_product()` 内部且 `cdp` 尚未返回上层,`open_product()` 必须自行清理本轮自动新建 tab;复用用户已有 tab 只断开 CDP,不关闭页面。失败现场可保存 HTML/toast JSON 片段,但不得记录 Cookie、密码、token。 4. **T-404b 已接入商品页失败 toast 捕获**:①采集和③更新在 `open_product`/等待详情页就绪失败时,不应只报等待超时。进入/刷新商品编辑页后应捕获 `.eds-toasts .eds-toast__content` 的文本和 `outerHTML`,记录当前 URL、时间、可见状态,并在关键元素超时时把最近错误 toast 作为用户可读失败原因写入 `last_error`、`run_log_events` 与本地诊断日志;例如商品 ID 失效时提示 `please input correct product id`。若 toast 明确属于商品失效/商品不存在/无权限类错误,底层仍写 `stage=imported/status=failed/last_error=商品失效:<原始toast>`,只在①导入采集列表“阶段”显示“商品失效”;其他打开失败仍显示“失败”。若失败发生在 `open_product()` 内部且 `cdp` 尚未返回上层,`open_product()` 必须自行清理本轮自动新建 tab;复用用户已有 tab 只断开 CDP,不关闭页面。失败现场可保存 HTML/toast JSON 片段,但不得记录 Cookie、密码、token。
@@ -433,7 +433,7 @@ data/images/<batch_id>/<slug>/<task_id>_<item_id>_new.<ext> # AI 生成的新
- 「检查本轮更新」只写 `run_logs/run_log_events` 和弹窗/状态栏检查汇总,不调用 `editor.apply_task()`,不做账号登录预检,不写任务状态,不回写 Excel。 - 「检查本轮更新」只写 `run_logs/run_log_events` 和弹窗/状态栏检查汇总,不调用 `editor.apply_task()`,不做账号登录预检,不写任务状态,不回写 Excel。
- 真实更新按每批最大条数分批执行,每条立即写 SQLite;全部完成回写 Excel(新字段+状态)+ 弹窗汇总。真实更新与检查都写运行日志,日志 payload 走脱敏工具。 - 真实更新按每批最大条数分批执行,每条立即写 SQLite;全部完成回写 Excel(新字段+状态)+ 弹窗汇总。真实更新与检查都写运行日志,日志 payload 走脱敏工具。
- 分批更新停止语义为协作式停止:点击停止后设置取消标记;当前正在执行的商品跑到安全边界后写库结束,不再开始新商品,也不进入下一批。未开始任务保持原状态,后续可继续。 - 分批更新停止语义为协作式停止:点击停止后设置取消标记;当前正在执行的商品跑到安全边界后写库结束,不再开始新商品,也不进入下一批。未开始任务保持原状态,后续可继续。
- T-404a 已在③提供「重置更新状态」:仅当前选中单条,保留 `new_title/new_cover_path`,本地退回 `stage=generated/status=pending` 以便重复测试上传/提交;若 `committed=1`,必须提示线上已提交过、本地重置不回滚 Shopee、重复更新会再次提交,并保留 committed 历史事实/运行日志。 - T-404a 已在③提供「重置更新状态」:仅当前选中单条,保留 `new_title/new_cover_path`,本地退回 `stage=generated/status=pending` 以便重复测试上传/提交;若 `committed=1`,必须提示线上已提交过、本地重置不回滚蝦皮、重复更新会再次提交,并保留 committed 历史事实/运行日志。
- 若 `close_success_tab=true`,且商品页是本轮程序自动新建、并已成功提交,则提交后关闭该商品编辑页;若确认后已跳回商品列表页,关闭前等待 2 秒;进入编辑页后的失败任务和复用的用户已有 tab 不关闭;`open_product` 内部打开失败的新建 tab 要关闭。 - 若 `close_success_tab=true`,且商品页是本轮程序自动新建、并已成功提交,则提交后关闭该商品编辑页;若确认后已跳回商品列表页,关闭前等待 2 秒;进入编辑页后的失败任务和复用的用户已有 tab 不关闭;`open_product` 内部打开失败的新建 tab 要关闭。
### 6.4 登录检测 ### 6.4 登录检测
@@ -470,7 +470,7 @@ data/images/<batch_id>/<slug>/<task_id>_<item_id>_new.<ext> # AI 生成的新
2. **账号与启动**(④):`config` 建目录、`chrome` 启动器/快捷方式、登录保活与检测、账号 CRUD。 2. **账号与启动**(④):`config` 建目录、`chrome` 启动器/快捷方式、登录保活与检测、账号 CRUD。
3. **导入采集**(①):`excel` 导入、采集旧标题/旧封面、回写。 3. **导入采集**(①):`excel` 导入、采集旧标题/旧封面、回写。
4. **AI 生成**(②):`ai` 模块、提示词、对照预览。 4. **AI 生成**(②):`ai` 模块、提示词、对照预览。
5. **更新 shopee**(③):对已生成任务弹窗批量确认后换标题+封面、提交、回写。 5. **更新蝦皮**(③):对已生成任务弹窗批量确认后换标题+封面、提交、回写。
6. **设置**(⑤)+ 首次未配账号引导保护。 6. **设置**(⑤)+ 首次未配账号引导保护。
## 九、项目结构建议 ## 九、项目结构建议
+2 -2
View File
@@ -27,7 +27,7 @@
## 3. 范围纪律 ## 3. 范围纪律
- V1 只做 `02-requirements.md` 中列为 P0 的当前目标功能(5 Tab、账号管理、Excel 导入采集、AI 生成、批量确认后更新 Shopee、结果回写)。 - V1 只做 `02-requirements.md` 中列为 P0 的当前目标功能(5 Tab、账号管理、Excel 导入采集、AI 生成、批量确认后更新蝦皮、结果回写)。
- 除 T-504 已接入的多账号并行、dry-run、运行日志外,其他 V2 / V3 功能(规则模板等)只记录,不实现。 - 除 T-504 已接入的多账号并行、dry-run、运行日志外,其他 V2 / V3 功能(规则模板等)只记录,不实现。
- 需求明确排除的非目标(自动登录、绕风控、商品数据批量爬取、外部数据库)不得实现。 - 需求明确排除的非目标(自动登录、绕风控、商品数据批量爬取、外部数据库)不得实现。
@@ -43,7 +43,7 @@
## 5. 代码规范 ## 5. 代码规范
- 标识符使用英文;UI 文案、注释、文档保持中文,与现有脚本一致。 - 标识符使用英文;UI 文案、注释、文档保持中文,与现有脚本一致。
- 所有用户可见文本必须使用中文:包括弹窗标题/正文/按钮、窗口标题、按钮、菜单、label、placeholder、tooltip、状态栏、空状态、确认框、运行日志、错误提示和成功提示等。不要直接使用 `Save` / `Discard` / `Cancel` 等英文标准按钮露出给用户;如需三选一弹窗,使用自定义中文按钮文案(如“保存”“放弃”“取消”)。URL、配置键、API 字段、模型别名、第三方/Shopee 原始错误可保留原文,但外围必须有中文解释。 - 所有用户可见文本必须使用中文:包括弹窗标题/正文/按钮、窗口标题、按钮、菜单、label、placeholder、tooltip、状态栏、空状态、确认框、运行日志、错误提示和成功提示等。不要直接使用 `Save` / `Discard` / `Cancel` 等英文标准按钮露出给用户;如需三选一弹窗,使用自定义中文按钮文案(如“保存”“放弃”“取消”)。运行程序后 GUI 上凡是面向用户的 `shopee` / `Shopee` 平台名称统一写「蝦皮」,例如「③ 更新蝦皮」「蝦皮更新安全」「未打开蝦皮」。URL、配置键、API 字段、模型别名、第三方/Shopee 原始错误可保留原文,但外围必须有中文解释。
- 错误必须处理:CDP 超时、tab 找不到、上传失败、按钮禁用都要给明确提示,不吞错。 - 错误必须处理:CDP 超时、tab 找不到、上传失败、按钮禁用都要给明确提示,不吞错。
- 注释解释“为什么”(尤其 CDP 的坑:代理、Origin、就绪判断、拖拽落点),不复述“做了什么”。 - 注释解释“为什么”(尤其 CDP 的坑:代理、Origin、就绪判断、拖拽落点),不复述“做了什么”。
- 复用现有脚本里已验证的 JS 字符串,不重写出不一致的版本。 - 复用现有脚本里已验证的 JS 字符串,不重写出不一致的版本。
+16 -15
View File
@@ -57,7 +57,7 @@
| T-204b | 采集完成后自动回写旧字段到 Excel | T-204 | `CollectWorker` 完成后自动触发 `excel.write_back()` 回写当前批次旧字段;成功时状态栏/日志提示“已回写”;原文件被锁时不影响 SQLite,提示关闭后点「回写旧数据到 Excel」手动重试或另存副本 | DONE | | T-204b | 采集完成后自动回写旧字段到 Excel | T-204 | `CollectWorker` 完成后自动触发 `excel.write_back()` 回写当前批次旧字段;成功时状态栏/日志提示“已回写”;原文件被锁时不影响 SQLite,提示关闭后点「回写旧数据到 Excel」手动重试或另存副本 | DONE |
| T-205 | 首次未配账号 / Chrome 未启动 / 未登录的引导保护 | T-105, T-203 | 无账号、匹配账号未启动 CDP 端口或未登录时,① 执行按钮禁用或采集前汇总提示,并引导去④;可提供“打开账号管理/启动登录”入口,但不无提示批量启动所有账号 Chrome | DONE | | T-205 | 首次未配账号 / Chrome 未启动 / 未登录的引导保护 | T-105, T-203 | 无账号、匹配账号未启动 CDP 端口或未登录时,① 执行按钮禁用或采集前汇总提示,并引导去④;可提供“打开账号管理/启动登录”入口,但不无提示批量启动所有账号 Chrome | DONE |
| T-205b | 采集后关闭程序自动新建的商品编辑页 tab | T-203 | `open_product` 区分复用旧 tab 与本次新建 tab;`CDP.close()` 仍只断开连接;采集完成后只关闭本次自动新建的商品页,不关闭用户原本打开的 tab;失败/异常也执行清理;③ 更新流程暂不自动关页,失败时保留现场便于排查 | DONE | | T-205b | 采集后关闭程序自动新建的商品编辑页 tab | T-203 | `open_product` 区分复用旧 tab 与本次新建 tab;`CDP.close()` 仍只断开连接;采集完成后只关闭本次自动新建的商品页,不关闭用户原本打开的 tab;失败/异常也执行清理;③ 更新流程暂不自动关页,失败时保留现场便于排查 | DONE |
| T-206 | Tab① 删除指定批次(软删除) | T-202, T-104, T-404 | 现状:导入后批次只增不减,下拉框无限膨胀、导错文件/测试导入无法清理。方案:`db.delete_batch(batch_id)` 改为软删除,在事务中给 `batches.deleted_at/deleted_reason` 写值,不物理删除 `batches/tasks`;默认 `list_batches/list_tasks` 以及①/②/③页面、筛选、采集、生成、更新、回写等业务入口都排除已删除批次,删除后用户不能在软件页面查看、筛选或再次调用该批任务。函数返回任务数、其中已上线(`committed=1`)任务数、关联封面图片绝对路径;Tab① 批次筛选旁加「删除批次」按钮,仅在选中具体批次(非「全部批次」)时可用,运行中禁用。二次确认弹窗显示批次标签、任务数、已上线任务数并提示「软删除只隐藏本地批次,不会回滚 Shopee 线上修改」;图片默认保留,不做自动清理,后续若需要清理图片另做独立工具。确认后软删除、刷新①/②/③列表和批次筛选 | DONE | | T-206 | Tab① 删除指定批次(软删除) | T-202, T-104, T-404 | 现状:导入后批次只增不减,下拉框无限膨胀、导错文件/测试导入无法清理。方案:`db.delete_batch(batch_id)` 改为软删除,在事务中给 `batches.deleted_at/deleted_reason` 写值,不物理删除 `batches/tasks`;默认 `list_batches/list_tasks` 以及①/②/③页面、筛选、采集、生成、更新、回写等业务入口都排除已删除批次,删除后用户不能在软件页面查看、筛选或再次调用该批任务。函数返回任务数、其中已上线(`committed=1`)任务数、关联封面图片绝对路径;Tab① 批次筛选旁加「删除批次」按钮,仅在选中具体批次(非「全部批次」)时可用,运行中禁用。二次确认弹窗显示批次标签、任务数、已上线任务数并提示「软删除只隐藏本地批次,不会回滚蝦皮 线上修改」;图片默认保留,不做自动清理,后续若需要清理图片另做独立工具。确认后软删除、刷新①/②/③列表和批次筛选 | DONE |
| T-207 | ① 采集诊断日志(run_logs + 本地 log) | T-203, T-503, T-504 | 问题:采集少量商品时单条失败只能看到 `tasks.last_error` 和状态栏计数,无法定位卡在打开商品页/页面就绪/读标题/读封面/下载图片/写库/回写哪一步。方案:`CollectWorker` 创建 `run_type=collect` 的运行日志,逐任务写 `run_log_events`(含 `task_id/alias/item_id` 和 `step=preflight/open_product/wait_ready/read_title/read_cover/download_cover/db_write/excel_write_back`);同时新增 gitignore 的 `logs/` 本地滚动日志,写脱敏 traceback 与耗时;GUI 至少能查看最近一次采集运行日志;失败仍按现有语义写 `status=failed/last_error`,不影响其他任务继续 | DONE | | T-207 | ① 采集诊断日志(run_logs + 本地 log) | T-203, T-503, T-504 | 问题:采集少量商品时单条失败只能看到 `tasks.last_error` 和状态栏计数,无法定位卡在打开商品页/页面就绪/读标题/读封面/下载图片/写库/回写哪一步。方案:`CollectWorker` 创建 `run_type=collect` 的运行日志,逐任务写 `run_log_events`(含 `task_id/alias/item_id` 和 `step=preflight/open_product/wait_ready/read_title/read_cover/download_cover/db_write/excel_write_back`);同时新增 gitignore 的 `logs/` 本地滚动日志,写脱敏 traceback 与耗时;GUI 至少能查看最近一次采集运行日志;失败仍按现有语义写 `status=failed/last_error`,不影响其他任务继续 | DONE |
## Phase 3 · AI 生成(②) ## Phase 3 · AI 生成(②)
@@ -68,16 +68,16 @@
| T-302 | Tab② 左右布局:左提示词(标题/封面),右按批次/店铺/状态筛选 + 任务列表 | T-301, T-203 | 左 ~1/4 提示词多行;右筛选+列表(店铺/商品id/旧标题/新标题/状态) | DONE | | T-302 | Tab② 左右布局:左提示词(标题/封面),右按批次/店铺/状态筛选 + 任务列表 | T-301, T-203 | 左 ~1/4 提示词多行;右筛选+列表(店铺/商品id/旧标题/新标题/状态) | DONE |
| T-302p | `app/prompts.py` + Tab② 提示词管理 | T-302 | 标题保存/启动回显 title_prompt.txt;封面多模板(下拉+新建/保存/另存为/重命名/删除,存 prompts/cover/);插入 `{新标题}`;预览变量替换;render_prompt 接入生成 | DONE | | T-302p | `app/prompts.py` + Tab② 提示词管理 | T-302 | 标题保存/启动回显 title_prompt.txt;封面多模板(下拉+新建/保存/另存为/重命名/删除,存 prompts/cover/);插入 `{新标题}`;预览变量替换;render_prompt 接入生成 | DONE |
| T-303 | Tab② 开始生成(单按钮)+ 停止 + 进度:**先并发标题再并发图片** | T-302, T-104b | `generate_batch` 先 title_concurrency 并发标题、再 image_concurrency 并发图片;worker/signal 回传进度;每条 set_generated 立即写库;停止可取消未开始项;进度 标题/封面/失败 计数;双击弹窗看新旧封面 | DONE | | T-303 | Tab② 开始生成(单按钮)+ 停止 + 进度:**先并发标题再并发图片** | T-302, T-104b | `generate_batch` 先 title_concurrency 并发标题、再 image_concurrency 并发图片;worker/signal 回传进度;每条 set_generated 立即写库;停止可取消未开始项;进度 标题/封面/失败 计数;双击弹窗看新旧封面 | DONE |
| T-303b | ②/③ 商品ID筛选 | T-303, T-401 | ② AI生成和③ 更新shopee筛选栏增加商品ID输入框;输入内容按包含匹配 `item_id`,清空表示全部;当前筛选结果和「开始生成」/「开始更新」范围都受其约束;③确认弹窗显示商品ID筛选条件 | DONE | | T-303b | ②/③ 商品ID筛选 | T-303, T-401 | ② AI生成和③ 更新蝦皮筛选栏增加商品ID输入框;输入内容按包含匹配 `item_id`,清空表示全部;当前筛选结果和「开始生成」/「开始更新」范围都受其约束;③确认弹窗显示商品ID筛选条件 | DONE |
## Phase 4 · 更新 shopee(③) ## Phase 4 · 更新蝦皮(③)
| ID | 任务 | 依赖 | 验收要点 | 状态 | | ID | 任务 | 依赖 | 验收要点 | 状态 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| T-401 | Tab③ 列出已生成任务 + 按批次/店铺/商品ID/状态筛选 + 开始更新确认弹窗 | T-303 | 顶部批次/店铺/商品ID/状态筛选;「开始更新」仅作用于当前筛选结果;弹窗显示筛选条件/任务数/线上提交风险;取消不执行;状态=失败可重试;无常驻提交开关 | DONE | | T-401 | Tab③ 列出已生成任务 + 按批次/店铺/商品ID/状态筛选 + 开始更新确认弹窗 | T-303 | 顶部批次/店铺/商品ID/状态筛选;「开始更新」仅作用于当前筛选结果;弹窗显示筛选条件/任务数/线上提交风险;取消不执行;状态=失败可重试;无常驻提交开关 | DONE |
| T-402 | 串行执行 apply:批量确认后换标题+换封面+点更新提交,单条失败继续,立即写库 | T-401, T-001, T-104b | 确认后 worker 串行执行并逐条 set_applied;失败继续;未确认时不调用 apply;真实更新前检查本轮账号 Chrome/CDP/登录态,未启动或未登录时整体阻断并提示去④,不自动打开 Chrome | DONE | | T-402 | 串行执行 apply:批量确认后换标题+换封面+点更新提交,单条失败继续,立即写库 | T-401, T-001, T-104b | 确认后 worker 串行执行并逐条 set_applied;失败继续;未确认时不调用 apply;真实更新前检查本轮账号 Chrome/CDP/登录态,未启动或未登录时整体阻断并提示去④,不自动打开 Chrome | DONE |
| T-403 | 回写结果到原 Excel(新标题/新封面/更新状态)+ 结束弹窗汇总 | T-402, T-204 | 回写原文件(锁处理);弹窗 成功/失败/略过 | DONE | | T-403 | 回写结果到原 Excel(新标题/新封面/更新状态)+ 结束弹窗汇总 | T-402, T-204 | 回写原文件(锁处理);弹窗 成功/失败/略过 | DONE |
| T-404a | ②/③ 选中记录重置(联调重试辅助) | T-303, T-403, T-504 | 初版解决 ② AI生成和③ 更新shopee列表无法重置选中记录的问题:③「重置更新状态」保留 `new_title/new_cover_path`,把本地状态退回可更新(`stage=generated/status=pending`、清空 `last_error`),用于重复测试上传/提交。②「重置生成结果」后续已由 T-534 升级,不再按初版退回 `collected` 或仅作用单选;当前以 T-534 为准:支持多选/当前筛选结果,确认框可选「重置标题 / 重置封面 / 重置全部」,只清对应 `new_title` 或 `new_cover_path` 与 `last_error`,保持 `stage=generated/status=success`,由 T-533 后续只补缺失组件;只重置封面时必须保留 `new_title` 和 T-509 手动标题。若记录 `committed=1`,确认框必须醒目提示“线上已提交过,本地重置不会回滚 Shopee,重复更新会再次提交线上”,并保留 committed 历史事实/运行日志;运行中禁用;两类重置都写 `run_type=reset` 运行日志,不触碰 Shopee、不回写 Excel,后续由手动回写同步本地状态 | DONE | | T-404a | ②/③ 选中记录重置(联调重试辅助) | T-303, T-403, T-504 | 初版解决 ② AI生成和③ 更新蝦皮列表无法重置选中记录的问题:③「重置更新状态」保留 `new_title/new_cover_path`,把本地状态退回可更新(`stage=generated/status=pending`、清空 `last_error`),用于重复测试上传/提交。②「重置生成结果」后续已由 T-534 升级,不再按初版退回 `collected` 或仅作用单选;当前以 T-534 为准:支持多选/当前筛选结果,确认框可选「重置标题 / 重置封面 / 重置全部」,只清对应 `new_title` 或 `new_cover_path` 与 `last_error`,保持 `stage=generated/status=success`,由 T-533 后续只补缺失组件;只重置封面时必须保留 `new_title` 和 T-509 手动标题。若记录 `committed=1`,确认框必须醒目提示“线上已提交过,本地重置不会回滚蝦皮,重复更新会再次提交线上”,并保留 committed 历史事实/运行日志;运行中禁用;两类重置都写 `run_type=reset` 运行日志,不触碰蝦皮、不回写 Excel,后续由手动回写同步本地状态 | DONE |
| T-404 | 真实 Shopee 单条更新冒烟验收 | T-403, T-501c | 已完成真实验收:2026-07-01 用户手动导入包含 5 个商品 ID 的 Excel,完成 AI 标题/图片生成,并通过 Tab③「开始更新」流程更新到 Shopee;覆盖真实编辑页、标题/封面更新、Shopee 站点侧确认框、提交后结果判断与本地状态链路。后续若 Shopee 页面结构变化,仍需按 CDP 规则补回归验证 | DONE | | T-404 | 真实 Shopee 单条更新冒烟验收 | T-403, T-501c | 已完成真实验收:2026-07-01 用户手动导入包含 5 个商品 ID 的 Excel,完成 AI 标题/图片生成,并通过 Tab③「开始更新」流程更新到 Shopee;覆盖真实编辑页、标题/封面更新、Shopee 站点侧确认框、提交后结果判断与本地状态链路。后续若 Shopee 页面结构变化,仍需按 CDP 规则补回归验证 | DONE |
| T-404b | 商品详情页加载失败 toast 自动捕获 | T-404, T-505 | 问题:③ 更新或①采集打开商品详情页时,如果商品 ID 已失效、无权限或店铺不匹配,Shopee 可能只弹出很快消失的错误 toast,例如 `please input correct product id`;用户来不及复制 HTML,GUI 也可能只报“商品详情页加载失败/等待超时”,无法判断真实原因。方案:在 `open_product()`/等待商品详情页就绪前安装或启用 toast 捕获,监听并兜底读取 `.eds-toasts .eds-toast__content`,保存最近 toast 的文本、`outerHTML`、当前 URL、时间和可见状态到页面内缓存;当标题输入框/图片管理器/上传入口等关键元素超时或页面不是商品编辑页时,优先把最近错误 toast 提升为失败原因,写入 `tasks.last_error`、`run_logs/run_log_events` 和本地 `logs/` 诊断日志,并可保存失败现场 HTML/toast JSON 片段用于排查。若 toast 明确属于商品失效/商品不存在/无权限类错误,则底层仍保持 `stage=imported/status=failed/last_error=商品失效:<原始toast>`,①导入采集列表“阶段”列显示“商品失效”;其他打开失败仍显示“失败”。如果失败发生在 `open_product()` 内部、尚未把 `cdp` 返回给上层,仍必须由 `open_product()` 自己清理:本轮自动新建的商品 tab 要断开 CDP 后关闭浏览器 target;复用用户原本打开的 tab 只断开 CDP,不关闭页面。不得记录 Cookie、密码、token;不绕过 Shopee 校验,不改变上传、删图、提交逻辑。同步 editor/CDP mock 和 GUI model 测试覆盖短暂 toast、隐藏 toast、商品 ID 失效错误文案上浮、自动新开 tab 在商品失效时关闭、复用 tab 不关闭、仅明确失效 toast 才显示“商品失效” | DONE | | T-404b | 商品详情页加载失败 toast 自动捕获 | T-404, T-505 | 问题:③ 更新或①采集打开商品详情页时,如果商品 ID 已失效、无权限或店铺不匹配,Shopee 可能只弹出很快消失的错误 toast,例如 `please input correct product id`;用户来不及复制 HTML,GUI 也可能只报“商品详情页加载失败/等待超时”,无法判断真实原因。方案:在 `open_product()`/等待商品详情页就绪前安装或启用 toast 捕获,监听并兜底读取 `.eds-toasts .eds-toast__content`,保存最近 toast 的文本、`outerHTML`、当前 URL、时间和可见状态到页面内缓存;当标题输入框/图片管理器/上传入口等关键元素超时或页面不是商品编辑页时,优先把最近错误 toast 提升为失败原因,写入 `tasks.last_error`、`run_logs/run_log_events` 和本地 `logs/` 诊断日志,并可保存失败现场 HTML/toast JSON 片段用于排查。若 toast 明确属于商品失效/商品不存在/无权限类错误,则底层仍保持 `stage=imported/status=failed/last_error=商品失效:<原始toast>`,①导入采集列表“阶段”列显示“商品失效”;其他打开失败仍显示“失败”。如果失败发生在 `open_product()` 内部、尚未把 `cdp` 返回给上层,仍必须由 `open_product()` 自己清理:本轮自动新建的商品 tab 要断开 CDP 后关闭浏览器 target;复用用户原本打开的 tab 只断开 CDP,不关闭页面。不得记录 Cookie、密码、token;不绕过 Shopee 校验,不改变上传、删图、提交逻辑。同步 editor/CDP mock 和 GUI model 测试覆盖短暂 toast、隐藏 toast、商品 ID 失效错误文案上浮、自动新开 tab 在商品失效时关闭、复用 tab 不关闭、仅明确失效 toast 才显示“商品失效” | DONE |
@@ -88,23 +88,23 @@
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| T-501 | Tab⑤ 设置 · AI 模型管理 UI(master-detail):模型下拉 + 新增/删除 + 详情(服务商名/类别/api_type/模型ID/网址/密钥打码/连接超时) + 测试连接 | T-005, T-104 | 复用 T-005 后端;name 唯一;类别 text/image;至少各一个;密钥本地明文保存、UI 打码;测试连接可用 | DONE | | T-501 | Tab⑤ 设置 · AI 模型管理 UI(master-detail):模型下拉 + 新增/删除 + 详情(服务商名/类别/api_type/模型ID/网址/密钥打码/连接超时) + 测试连接 | T-005, T-104 | 复用 T-005 后端;name 唯一;类别 text/image;至少各一个;密钥本地明文保存、UI 打码;测试连接可用 | DONE |
| T-501b | Tab⑤ 设置 · 角色与生成参数:标题大模型/图片大模型下拉、分辨率(512/1k/2k/4k)、返回超时随分辨率、并发/重试/jpg质量、路径/端口 | T-501 | 角色下拉按类别过滤;分辨率选后返回超时按 resolution_timeouts 自动;持久化 config.json | DONE | | T-501b | Tab⑤ 设置 · 角色与生成参数:标题大模型/图片大模型下拉、分辨率(512/1k/2k/4k)、返回超时随分辨率、并发/重试/jpg质量、路径/端口 | T-501 | 角色下拉按类别过滤;分辨率选后返回超时按 resolution_timeouts 自动;持久化 config.json | DONE |
| T-501c | Tab⑤ 设置 · Shopee 更新安全开关 | T-501b | 配置测试商品 ID(历史/调试字段)、是否允许真实提交、是否允许更新封面、单次最大更新条数、成功后是否自动关闭本次新开编辑 tab;默认关闭真实提交和封面更新;Tab③ 执行前读取并拦截不符合安全配置的更新 | DONE | | T-501c | Tab⑤ 设置 · 蝦皮更新安全开关 | T-501b | 配置测试商品 ID(历史/调试字段)、是否允许真实提交、是否允许更新封面、单次最大更新条数、成功后是否自动关闭本次新开编辑 tab;默认关闭真实提交和封面更新;Tab③ 执行前读取并拦截不符合安全配置的更新 | DONE |
| T-502 | 换封面:删第一张再上传 | T-001 | 已实现备份校验、删第一张、确认弹窗、再上传和拖首位;更新封面统一先删当前第一张,不再只限满 9 张;删除前必须确认该任务已有本地旧封面备份(`old_cover_path` 存在且文件存在),缺失则拒绝删除并报错;已在 9 图测试商品 `29671243750` 上实测不提交流程 | DONE | | T-502 | 换封面:删第一张再上传 | T-001 | 已实现备份校验、删第一张、确认弹窗、再上传和拖首位;更新封面统一先删当前第一张,不再只限满 9 张;删除前必须确认该任务已有本地旧封面备份(`old_cover_path` 存在且文件存在),缺失则拒绝删除并报错;已在 9 图测试商品 `29671243750` 上实测不提交流程 | DONE |
| T-503 | 敏感信息本地明文存储提示与日志脱敏 | T-105, T-501 | 首次保存密码/API Key 时提示“本地明文保存”;UI 打码;日志/导出不含密码/API Key;文档说明 `config.json`/`config/ai_models.json`/DB/user-data-dir/images 必须 gitignore | DONE | | T-503 | 敏感信息本地明文存储提示与日志脱敏 | T-105, T-501 | 首次保存密码/API Key 时提示“本地明文保存”;UI 打码;日志/导出不含密码/API Key;文档说明 `config.json`/`config/ai_models.json`/DB/user-data-dir/images 必须 gitignore | DONE |
| T-504 | 多账号并行 / dry-run / 运行日志(V2) | T-402 | 端口不冲突;dry-run 只预览;操作留痕 | DONE | | T-504 | 多账号并行 / dry-run / 运行日志(V2) | T-402 | 端口不冲突;dry-run 只预览;操作留痕 | DONE |
| T-505 | 全流程诊断日志扩展 | T-207, T-504 | 在 T-207 采集日志机制稳定后,已扩展到② AI生成、Excel 导入/回写、③ 更新shopee步骤级日志、④ Chrome 启动/登录检测、⑤ AI模型测试连接;数据库日志保存业务可读步骤和错误,本地 log 保存脱敏 traceback;不得记录 Cookie、密码、API Key、token | DONE | | T-505 | 全流程诊断日志扩展 | T-207, T-504 | 在 T-207 采集日志机制稳定后,已扩展到② AI生成、Excel 导入/回写、③ 更新蝦皮步骤级日志、④ Chrome 启动/登录检测、⑤ AI模型测试连接;数据库日志保存业务可读步骤和错误,本地 log 保存脱敏 traceback;不得记录 Cookie、密码、API Key、token | DONE |
| T-506 | 正式使用批量更新体验:③ 检查本轮更新 + 分批更新全部 + ⑤ 设置页三列布局 | T-501c, T-504 | ③ 增加「检查本轮更新」按钮替代用户可见 dry-run;「开始更新」对当前筛选结果按每批最大更新条数自动分批,确认弹窗显示总数/每批/预计批次,停止为当前商品安全结束后不再开始新任务;⑤ 设置页使用居中内容区,左右留白已缩短为 T-506 初始实现约 40%,模型详情/角色与生成参数/路径与端口/Shopee 更新安全均使用响应式三列布局,长字段跨列;「多账号并行更新」与「最大并行账号数」合并为同一个横向组件,最大并行账号数紧跟其后且不换行 | DONE | | T-506 | 正式使用批量更新体验:③ 检查本轮更新 + 分批更新全部 + ⑤ 设置页三列布局 | T-501c, T-504 | ③ 增加「检查本轮更新」按钮替代用户可见 dry-run;「开始更新」对当前筛选结果按每批最大更新条数自动分批,确认弹窗显示总数/每批/预计批次,停止为当前商品安全结束后不再开始新任务;⑤ 设置页使用居中内容区,左右留白已缩短为 T-506 初始实现约 40%,模型详情/角色与生成参数/路径与端口/蝦皮更新安全均使用响应式三列布局,长字段跨列;「多账号并行更新」与「最大并行账号数」合并为同一个横向组件,最大并行账号数紧跟其后且不换行 | DONE |
| T-507 | 正式批量更新:移除普通流程测试商品 ID 限制 | T-506 | 普通正式更新不再以 `test_item_id` 阻断当前筛选结果;③ 确认弹窗不再显示测试商品 ID;⑤ 普通设置页隐藏测试商品 ID,仅保留历史/调试兼容字段;仍保留允许真实提交、允许更新封面、分批、二次确认、账号就绪预检和运行日志 | DONE | | T-507 | 正式批量更新:移除普通流程测试商品 ID 限制 | T-506 | 普通正式更新不再以 `test_item_id` 阻断当前筛选结果;③ 确认弹窗不再显示测试商品 ID;⑤ 普通设置页隐藏测试商品 ID,仅保留历史/调试兼容字段;仍保留允许真实提交、允许更新封面、分批、二次确认、账号就绪预检和运行日志 | DONE |
| T-508 | ③ 更新shopee生产化操作区 | T-401, T-404a, T-506 | 「开始更新」作为主操作视觉强化;「重置更新状态」从底部按钮移到任务表右键菜单/高级入口,与批量更新物理分离,运行中禁用;更新安全拦截弹窗写明具体开关并提供「前往设置」深链到⑤,不改 Shopee/CDP 更新流程 | DONE | | T-508 | ③ 更新蝦皮生产化操作区 | T-401, T-404a, T-506 | 「开始更新」作为主操作视觉强化;「重置更新状态」从底部按钮移到任务表右键菜单/高级入口,与批量更新物理分离,运行中禁用;更新安全拦截弹窗写明具体开关并提供「前往设置」深链到⑤,不改 Shopee/CDP 更新流程 | DONE |
| T-509 | ② 新标题人工微调 | T-303, T-401 | ②任务表「新标题」列可编辑;仅允许已生成、未提交线上、非运行中任务;编辑写回 `tasks.new_title`,保留 `stage=generated`,清空 `last_error`,状态回到可更新;不触碰 Shopee/CDP/Excel,③ 使用修改后的标题 | DONE | | T-509 | ② 新标题人工微调 | T-303, T-401 | ②任务表「新标题」列可编辑;仅允许已生成、未提交线上、非运行中任务;编辑写回 `tasks.new_title`,保留 `stage=generated`,清空 `last_error`,状态回到可更新;不触碰蝦皮/CDP/Excel,③ 使用修改后的标题 | DONE |
| T-510 | ③ 检查本轮更新文案统一 | T-506, T-508 | 将用户可见「预览本轮更新」统一改为「检查本轮更新」,按钮、确认框、状态栏、完成弹窗、运行日志和文档一致;保留内部 `dry_run` 字段与执行语义,不改 Shopee/CDP 更新流程 | DONE | | T-510 | ③ 检查本轮更新文案统一 | T-506, T-508 | 将用户可见「预览本轮更新」统一改为「检查本轮更新」,按钮、确认框、状态栏、完成弹窗、运行日志和文档一致;保留内部 `dry_run` 字段与执行语义,不改 Shopee/CDP 更新流程 | DONE |
| T-511 | 语义色板 + ①②③任务状态列上色 | T-202, T-303, T-401 | 依据 `docs/ui-color-design.md`。在 `app/gui.py` 顶部定义统一语义色板常量(success `#1a7f37`/danger `#cf222e`/info `#0969da`/pending `#9a6700`/muted `#6e7781`/warning `#bc4c00`),全局引用不散落硬编码。①②③ 三个 TableModel 在 `Qt.ForegroundRole` 按内部 `stage/status` 返回 `QColor`,不要按中文显示文案硬匹配:完成态=success、失败=danger、略过/取消=muted、待处理=pending、进行中=info;不给普通行刷底色(失败行浅底 `#ffebe9` 可选、仅此一种)。保持文字+颜色双编码,不只靠颜色。跑 `test_gui.py` 无回归 | DONE | | T-511 | 语义色板 + ①②③任务状态列上色 | T-202, T-303, T-401 | 依据 `docs/ui-color-design.md`。在 `app/gui.py` 顶部定义统一语义色板常量(success `#1a7f37`/danger `#cf222e`/info `#0969da`/pending `#9a6700`/muted `#6e7781`/warning `#bc4c00`),全局引用不散落硬编码。①②③ 三个 TableModel 在 `Qt.ForegroundRole` 按内部 `stage/status` 返回 `QColor`,不要按中文显示文案硬匹配:完成态=success、失败=danger、略过/取消=muted、待处理=pending、进行中=info;不给普通行刷底色(失败行浅底 `#ffebe9` 可选、仅此一种)。保持文字+颜色双编码,不只靠颜色。跑 `test_gui.py` 无回归 | DONE |
| T-512 | ③高风险按钮上色 + ①导入校验数字标红 | T-511, T-508, T-202b | ③「开始更新」(写线上)改用 warning `#bc4c00` 系强调(描边或填充;第一版不强制图标,避免 Windows/Qt 字体渲染不稳),与②安全流程 info 蓝主操作区分;「停止」保持中性。①导入汇总栏:未匹配数 >0 时用 danger 且保持现有按钮可点击筛出,=0 保持中性;无效行数 >0 时只标红提示,不承诺在任务表筛出,因为脏行未入库,后续若要点击查看需另做「导入错误明细」。颜色取自 T-511 色板常量 | DONE | | T-512 | ③高风险按钮上色 + ①导入校验数字标红 | T-511, T-508, T-202b | ③「开始更新」(写线上)改用 warning `#bc4c00` 系强调(描边或填充;第一版不强制图标,避免 Windows/Qt 字体渲染不稳),与②安全流程 info 蓝主操作区分;「停止」保持中性。①导入汇总栏:未匹配数 >0 时用 danger 且保持现有按钮可点击筛出,=0 保持中性;无效行数 >0 时只标红提示,不承诺在任务表筛出,因为脏行未入库,后续若要点击查看需另做「导入错误明细」。颜色取自 T-511 色板常量 | DONE |
| T-513 | 登录点 / ③Tab危险标识 / 破坏性按钮上色 | T-511, T-105, T-206 | ④登录状态 `●` 已登录=success、未登录/未检测=danger 或 muted(保留文字);③ 更新shopee Tab 若做危险标识,不依赖 QSS 硬选第 3 个 Tab,优先用 `QTabWidget.setTabIcon(2, ...)` 设置克制 warning 小圆点图标,或第一版先跳过 Tab 标识;不整条刷红。破坏性按钮「删除批次」「删除账号」文字/描边用 danger,与普通灰按钮区分(二次确认弹窗仍是主防线)。颜色取自 T-511 色板常量 | DONE | | T-513 | 登录点 / ③Tab危险标识 / 破坏性按钮上色 | T-511, T-105, T-206 | ④登录状态 `●` 已登录=success、未登录/未检测=danger 或 muted(保留文字);③ 更新蝦皮 Tab 若做危险标识,不依赖 QSS 硬选第 3 个 Tab,优先用 `QTabWidget.setTabIcon(2, ...)` 设置克制 warning 小圆点图标,或第一版先跳过 Tab 标识;不整条刷红。破坏性按钮「删除批次」「删除账号」文字/描边用 danger,与普通灰按钮区分(二次确认弹窗仍是主防线)。颜色取自 T-511 色板常量 | DONE |
| T-514 | ①②③ 首次空状态引导卡片 | T-205 | 依据 `docs/ux-review.md` P1。①②③ 增加轻量空状态引导卡片,不改采集/生成/更新流程和既有预检拦截逻辑。无账号时三页均显示「第一步:前往『④账号管理』配置并登录账号」并提供「前往账号管理」按钮;有账号但无任务时,①提示导入 Excel,②提示先完成①采集,③提示先完成②生成;账号/任务就绪后卡片自动隐藏,老用户无感。卡片只做 UI 引导,不替代执行前账号 Chrome/CDP/登录态检查 | DONE | | T-514 | ①②③ 首次空状态引导卡片 | T-205 | 依据 `docs/ux-review.md` P1。①②③ 增加轻量空状态引导卡片,不改采集/生成/更新流程和既有预检拦截逻辑。无账号时三页均显示「第一步:前往『④账号管理』配置并登录账号」并提供「前往账号管理」按钮;有账号但无任务时,①提示导入 Excel,②提示先完成①采集,③提示先完成②生成;账号/任务就绪后卡片自动隐藏,老用户无感。卡片只做 UI 引导,不替代执行前账号 Chrome/CDP/登录态检查 | DONE |
| T-515 | 批次阶段进度总览 | T-206, T-401 | 依据 `docs/ux-review.md` P1。加轻量批次进度总览:①②③在当前批次/全部批次摘要区下方显示同一组阶段计数,按当前批次筛选聚合总数、导入、已采集、已生成、已更新、失败、略过。计数口径:失败/略过优先按 `status=failed/skipped` 统计;已更新按 `stage=applied` 且非失败/略过;已生成按 `stage=generated` 且非失败/略过;已采集按 `stage=collected` 且非失败/略过;导入按其余未进入后续阶段任务统计。数据由现有 `db.list_tasks(batch_id=...)` 聚合,不新增表;已软删除批次不计入;只做 UI 总览,不改采集/生成/更新执行流程 | DONE | | T-515 | 批次阶段进度总览 | T-206, T-401 | 依据 `docs/ux-review.md` P1。加轻量批次进度总览:①②③在当前批次/全部批次摘要区下方显示同一组阶段计数,按当前批次筛选聚合总数、导入、已采集、已生成、已更新、失败、略过。计数口径:失败/略过优先按 `status=failed/skipped` 统计;已更新按 `stage=applied` 且非失败/略过;已生成按 `stage=generated` 且非失败/略过;已采集按 `stage=collected` 且非失败/略过;导入按其余未进入后续阶段任务统计。数据由现有 `db.list_tasks(batch_id=...)` 聚合,不新增表;已软删除批次不计入;只做 UI 总览,不改采集/生成/更新执行流程 | DONE |
| T-516 | ①筛选对齐②③ | T-202, T-303b | 依据 `docs/ux-review.md` P2。① 导入采集补齐与②③一致的店铺、商品ID、状态筛选,保持三个列表页筛选心智一致。筛选栏顺序为批次/店铺/商品ID/状态;当前筛选结果用于①表格显示与「采集旧标题/旧封面」作用范围;导入汇总栏、批次进度总览、未匹配数量仍按当前批次全量任务统计,未匹配按钮继续筛出当前筛选结果中的未匹配任务;不改导入、采集、回写、CDP 或 Shopee 更新逻辑 | DONE | | T-516 | ①筛选对齐②③ | T-202, T-303b | 依据 `docs/ux-review.md` P2。① 导入采集补齐与②③一致的店铺、商品ID、状态筛选,保持三个列表页筛选心智一致。筛选栏顺序为批次/店铺/商品ID/状态;当前筛选结果用于①表格显示与「采集旧标题/旧封面」作用范围;导入汇总栏、批次进度总览、未匹配数量仍按当前批次全量任务统计,未匹配按钮继续筛出当前筛选结果中的未匹配任务;不改导入、采集、回写、CDP 或 Shopee 更新逻辑 | DONE |
| T-517 | ⑤设置分区 + 清理兼容字段 | T-506, T-507 | 依据 `docs/ux-review.md` P2。⑤ 视觉分区:将高频「Shopee 更新安全 / 执行模式」前置成独立分区,低频「基础设施(Chrome路径/端口/DB路径)」独立后置;彻底隐藏 `test_item_id`、`dry_run` 用户入口,不再占用可见表单位置,但保留 `config.json` 内部兼容字段与③「检查本轮更新」语义。只改⑤设置页 UI 分区、保存/加载展示和 GUI 测试,不改配置 schema,不改 Shopee/CDP 更新流程 | DONE | | T-517 | ⑤设置分区 + 清理兼容字段 | T-506, T-507 | 依据 `docs/ux-review.md` P2。⑤ 视觉分区:将高频「蝦皮更新安全 / 执行模式」前置成独立分区,低频「基础设施(Chrome路径/端口/DB路径)」独立后置;彻底隐藏 `test_item_id`、`dry_run` 用户入口,不再占用可见表单位置,但保留 `config.json` 内部兼容字段与③「检查本轮更新」语义。只改⑤设置页 UI 分区、保存/加载展示和 GUI 测试,不改配置 schema,不改 Shopee/CDP 更新流程 | DONE |
| T-518 | ②左栏提示词区组件密度优化 | T-302p, T-303, T-517 | 依据 `docs/ux-review.md` P2。② AI生成左栏保留高频「模板选择、编辑内容、保存当前模板/开始生成」可见入口;将封面模板低频操作「另存为、重命名、删除」收敛进一个「模板操作」菜单/小按钮,避免左栏按钮堆叠和误点。第一版不迁移到⑤设置,不改 prompts 数据结构,不改 AI 生成 HTTP、DB schema、Excel、Shopee/CDP 流程;删除仍保留现有二次确认,保存/重命名/删除行为和错误处理不变。同步 GUI 测试覆盖菜单入口、动作触发和旧低频按钮不再平铺 | DONE | | T-518 | ②左栏提示词区组件密度优化 | T-302p, T-303, T-517 | 依据 `docs/ux-review.md` P2。② AI生成左栏保留高频「模板选择、编辑内容、保存当前模板/开始生成」可见入口;将封面模板低频操作「另存为、重命名、删除」收敛进一个「模板操作」菜单/小按钮,避免左栏按钮堆叠和误点。第一版不迁移到⑤设置,不改 prompts 数据结构,不改 AI 生成 HTTP、DB schema、Excel、Shopee/CDP 流程;删除仍保留现有二次确认,保存/重命名/删除行为和错误处理不变。同步 GUI 测试覆盖菜单入口、动作触发和旧低频按钮不再平铺 | DONE |
| T-519 | ②AI生成长任务进度条 + 用户可读滚动日志 | T-303, T-505, T-518 | 问题:批量生成几十/上百个标题或图片超时重试时,仅底部文字 `进度:标题x/y · 封面x/y · 失败z` 和技术型结构化日志不足以让用户判断剩余量、是否卡住、慢在哪一步。方案:②底部操作区增加标题/图片两条独立 `QProgressBar`,分两行显示 `标题 x/y`、`图片 x/y`,标题阶段结束后保持 100%,图片进度按本轮总任务口径显示并在旁边保留失败数;现有「AI生成运行日志」改为用户可读自动滚动日志,显示本轮开始、标题/图片开始与成功、商品ID/店铺、图片超时/重试、失败原因、停止请求和完成汇总。日志不得显示 API Key、密码、Cookie、token、完整请求体、base64 图片或超长 prompt;保留 DB `run_logs/run_log_events` 脱敏记录。只改②生成 UI、`GenerateWorker`/AI 生成事件文案和 GUI/AI 单测,不改 AI HTTP 协议、DB schema、Excel、Shopee/CDP 流程 | DONE | | T-519 | ②AI生成长任务进度条 + 用户可读滚动日志 | T-303, T-505, T-518 | 问题:批量生成几十/上百个标题或图片超时重试时,仅底部文字 `进度:标题x/y · 封面x/y · 失败z` 和技术型结构化日志不足以让用户判断剩余量、是否卡住、慢在哪一步。方案:②底部操作区增加标题/图片两条独立 `QProgressBar`,分两行显示 `标题 x/y`、`图片 x/y`,标题阶段结束后保持 100%,图片进度按本轮总任务口径显示并在旁边保留失败数;现有「AI生成运行日志」改为用户可读自动滚动日志,显示本轮开始、标题/图片开始与成功、商品ID/店铺、图片超时/重试、失败原因、停止请求和完成汇总。日志不得显示 API Key、密码、Cookie、token、完整请求体、base64 图片或超长 prompt;保留 DB `run_logs/run_log_events` 脱敏记录。只改②生成 UI、`GenerateWorker`/AI 生成事件文案和 GUI/AI 单测,不改 AI HTTP 协议、DB schema、Excel、Shopee/CDP 流程 | DONE |
| T-520 | ②AI生成封面可选生成开关 | T-519, T-501b | 问题:图片生成成本明显高于标题生成,当前②「开始生成」默认标题和图片都生成,用户只想改标题时也会调用图片模型,成本不可控;⑤「允许更新封面」只控制③线上提交阶段,不能替代②生成阶段的成本选择。方案:②AI生成页增加「生成封面图片(成本较高)」复选框,状态持久化到 `config.json` 的 `ai.generate_cover`,默认 `false`;点击「开始生成」时读取该开关:关闭时只并发生成标题,标题成功后立即 `db.set_generated(task_id, new_title, NULL)`,任务进入 `generated` 并可在③只更新标题,不调用 `gen_cover()`、不渲染封面提示词、不创建新封面文件,图片进度和运行日志明确显示本轮未生成图片;开启时保持现有标题后图片两段流程。③的 `allow_cover_update` 仍只控制线上更新阶段,若任务无 `new_cover_path`,③跳过封面更新且不因未开启「允许更新封面」阻断。只改 `config.json` 的 AI 段、②生成 UI、`GenerateWorker`/`ai.generate_batch()` 编排和 `appconfig/ai/gui` 单测;不改 DB schema、AI HTTP 协议、Excel、Shopee/CDP 更新流程 | DONE | | T-520 | ②AI生成封面可选生成开关 | T-519, T-501b | 问题:图片生成成本明显高于标题生成,当前②「开始生成」默认标题和图片都生成,用户只想改标题时也会调用图片模型,成本不可控;⑤「允许更新封面」只控制③线上提交阶段,不能替代②生成阶段的成本选择。方案:②AI生成页增加「生成封面图片(成本较高)」复选框,状态持久化到 `config.json` 的 `ai.generate_cover`,默认 `false`;点击「开始生成」时读取该开关:关闭时只并发生成标题,标题成功后立即 `db.set_generated(task_id, new_title, NULL)`,任务进入 `generated` 并可在③只更新标题,不调用 `gen_cover()`、不渲染封面提示词、不创建新封面文件,图片进度和运行日志明确显示本轮未生成图片;开启时保持现有标题后图片两段流程。③的 `allow_cover_update` 仍只控制线上更新阶段,若任务无 `new_cover_path`,③跳过封面更新且不因未开启「允许更新封面」阻断。只改 `config.json` 的 AI 段、②生成 UI、`GenerateWorker`/`ai.generate_batch()` 编排和 `appconfig/ai/gui` 单测;不改 DB schema、AI HTTP 协议、Excel、Shopee/CDP 更新流程 | DONE |
@@ -116,7 +116,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-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-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-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-523a | ②/③ 新一轮运行前清空界面日志显示 | T-519, T-505, T-523 | 问题:②AI生成和③更新蝦皮进入模块或启动新一轮任务时仍显示上一次运行日志,用户容易把旧失败、旧进度误认为本轮状态。方案:点击②「开始生成」、③「检查本轮更新」或③「开始更新」时,先清空对应界面的可见日志显示区,并写入本轮开始摘要;运行中只追加本轮日志,完成后保留本轮日志。只清空界面文本,不删除 SQLite `run_logs/run_log_events` 或本地 `logs/` 诊断日志;进入页面默认可显示“本轮日志会在开始运行后显示”,历史日志查看作为后续低优先级入口,不自动混入当前运行界面。同步 GUI 测试覆盖新一轮运行不会混入旧日志 | DONE |
| T-524 | PyInstaller 打包为免安装 exe | T-521 | 依据 `docs/engineering-review.md` P1。新增 PyInstaller spec/脚本,产出 Windows onedir 免安装 `.exe`;打包排除并绝不内置 `config.json`/`config/ai_models.json`/`cmshopee.db*`/`chrome_user_data_dir/`/`images/`/`logs/`/`prompts/`/`title_prompt.txt` 等本地数据与密钥;打包版工作目录切到 exe 所在目录,首次运行按现有默认值在本地生成配置;`scripts/build_exe.ps1` 可构建并校验发布目录,`docs/packaging.md` 说明分发和用户手动覆盖更新步骤 | DONE | | T-524 | PyInstaller 打包为免安装 exe | T-521 | 依据 `docs/engineering-review.md` P1。新增 PyInstaller spec/脚本,产出 Windows onedir 免安装 `.exe`;打包排除并绝不内置 `config.json`/`config/ai_models.json`/`cmshopee.db*`/`chrome_user_data_dir/`/`images/`/`logs/`/`prompts/`/`title_prompt.txt` 等本地数据与密钥;打包版工作目录切到 exe 所在目录,首次运行按现有默认值在本地生成配置;`scripts/build_exe.ps1` 可构建并校验发布目录,`docs/packaging.md` 说明分发和用户手动覆盖更新步骤 | DONE |
## Phase 7 · AI 网关对接(`docs/cmhub-integration-design.md`) ## Phase 7 · AI 网关对接(`docs/cmhub-integration-design.md`)
@@ -130,10 +130,10 @@
| T-528 | ② 计费错误提示 + 余额展示 | T-526, T-527, T-303 | 依据 `docs/cmhub-integration-design.md` v3.2。② AI生成页把 cmhub 计费失败态显式化:通过 `CMHubError.code` 识别 `insufficient_points`,弹明确提示「点数不足,请先充值」并引导去网页端充值,本轮未开始任务可提前中止,不靠中文字符串匹配、不淹没在失败计数里;用 T-526 成功响应事件里的 `points_balance` 刷新②页剩余点数显示,`/balance` 仅作手动刷新/可选批量前预检;`points_cost`/`call_id` 记入脱敏 run_logs。只改② UI、`GenerateWorker` 事件处理/文案及 GUI 单测;不改 AI HTTP 协议、DB schema、Excel、Shopee/CDP 流程 | DONE | | T-528 | ② 计费错误提示 + 余额展示 | T-526, T-527, T-303 | 依据 `docs/cmhub-integration-design.md` v3.2。② AI生成页把 cmhub 计费失败态显式化:通过 `CMHubError.code` 识别 `insufficient_points`,弹明确提示「点数不足,请先充值」并引导去网页端充值,本轮未开始任务可提前中止,不靠中文字符串匹配、不淹没在失败计数里;用 T-526 成功响应事件里的 `points_balance` 刷新②页剩余点数显示,`/balance` 仅作手动刷新/可选批量前预检;`points_cost`/`call_id` 记入脱敏 run_logs。只改② UI、`GenerateWorker` 事件处理/文案及 GUI 单测;不改 AI HTTP 协议、DB schema、Excel、Shopee/CDP 流程 | DONE |
| T-529 | 默认 cmhub 网关并隐藏 AI 后端选择 | T-528, T-527 | 产品收口:后续普通用户只使用 cmhub 网关,不再在⑤设置页暴露「AI 后端」label 与 direct/cmhub 下拉框。方案:`DEFAULT_CONFIG.ai.backend` 改为 `cmhub`;⑤设置页默认直接展示 cmhub 网关配置(Base URL、API Key、生文/生图别名、刷新别名、测试连接/查余额),隐藏 direct 模型选择、模型详情、标题/图片模型角色下拉和「AI 后端」下拉;保存设置固定写 `ai.backend=cmhub`,允许先保存不完整 cmhub 配置,②生成时仍由 `app/ai.py` 对缺 Base URL/Key/别名给出「请去⑤配置 cmhub」错误。direct 代码、`config/ai_models.json` helper 与 direct 单测保留为内部兼容/手工回滚路径,但普通 UI 不提供切换入口。同步 `appconfig`、⑤设置页、②余额默认显示和 GUI/appconfig 单测;不改 AI HTTP 协议、DB schema、Excel、Shopee/CDP 流程 | DONE | | T-529 | 默认 cmhub 网关并隐藏 AI 后端选择 | T-528, T-527 | 产品收口:后续普通用户只使用 cmhub 网关,不再在⑤设置页暴露「AI 后端」label 与 direct/cmhub 下拉框。方案:`DEFAULT_CONFIG.ai.backend` 改为 `cmhub`;⑤设置页默认直接展示 cmhub 网关配置(Base URL、API Key、生文/生图别名、刷新别名、测试连接/查余额),隐藏 direct 模型选择、模型详情、标题/图片模型角色下拉和「AI 后端」下拉;保存设置固定写 `ai.backend=cmhub`,允许先保存不完整 cmhub 配置,②生成时仍由 `app/ai.py` 对缺 Base URL/Key/别名给出「请去⑤配置 cmhub」错误。direct 代码、`config/ai_models.json` helper 与 direct 单测保留为内部兼容/手工回滚路径,但普通 UI 不提供切换入口。同步 `appconfig`、⑤设置页、②余额默认显示和 GUI/appconfig 单测;不改 AI HTTP 协议、DB schema、Excel、Shopee/CDP 流程 | DONE |
| T-530 | cmhub Base URL 规整 + 404 明确提示 | T-526, T-527 | 现象:手动填入 Base URL 后点「刷新别名」提示 notfound(HTTP 404)。核对确认 cmshopee 请求已达标(`GET /api/v1/models` + Bearer,对齐对接文档 §4.4 与 cmhub `ModelsView` 路由,见 `docs/troubleshooting.md`「cmhub 刷新别名 notfound」与 `docs/cmhub-integration-design.md` v3.4);404 根因是 Base URL 带多余 `/api(/v1)` 路径导致双拼、或所连实例未部署 `/api/v1/models`。方案:保存/请求前规整 Base URL——去掉结尾的 `/`、`/api`、`/api/v1` 等多余路径段,只保留 scheme+host(+port)(`appconfig.cmhub_request_url()` 或保存时统一处理),并在⑤输入框旁给出「只填网关根,如 https://host」提示;`_cmhub_code_for_status` 给 404 一个明确码/中文提示(如「cmhub 接口不存在,请检查 Base URL 或该实例是否已部署 /api/v1/models」),不再透传生涩英文原文。补 `test_appconfig`(Base URL 规整:带 `/api/v1`、带尾斜杠、带路径均归一到根)与 `test_ai`(404 → 明确提示)单测;不改 AI HTTP 协议、DB schema、Excel、Shopee/CDP 流程 | DONE | | T-530 | cmhub Base URL 规整 + 404 明确提示 | T-526, T-527 | 现象:手动填入 Base URL 后点「刷新别名」提示 notfound(HTTP 404)。核对确认 cmshopee 请求已达标(`GET /api/v1/models` + Bearer,对齐对接文档 §4.4 与 cmhub `ModelsView` 路由,见 `docs/troubleshooting.md`「cmhub 刷新别名 notfound」与 `docs/cmhub-integration-design.md` v3.4);404 根因是 Base URL 带多余 `/api(/v1)` 路径导致双拼、或所连实例未部署 `/api/v1/models`。方案:保存/请求前规整 Base URL——去掉结尾的 `/`、`/api`、`/api/v1` 等多余路径段,只保留 scheme+host(+port)(`appconfig.cmhub_request_url()` 或保存时统一处理),并在⑤输入框旁给出「只填网关根,如 https://host」提示;`_cmhub_code_for_status` 给 404 一个明确码/中文提示(如「cmhub 接口不存在,请检查 Base URL 或该实例是否已部署 /api/v1/models」),不再透传生涩英文原文。补 `test_appconfig`(Base URL 规整:带 `/api/v1`、带尾斜杠、带路径均归一到根)与 `test_ai`(404 → 明确提示)单测;不改 AI HTTP 协议、DB schema、Excel、Shopee/CDP 流程 | DONE |
| T-531 | ⑤ 设置未保存状态追踪 + 离开确认 | T-530, T-529, T-527 | 现象:在⑤填写 cmhub Base URL/API Key(或改任何设置)后,没有任何"未保存"提示;切换到其它 Tab、关闭窗口都不拦截,输入静默丢失。且「刷新别名/测试连接」用输入框实时文本、不落盘,成功后易误以为已保存。采用**方式 B(显式保存 + 脏状态追踪 + 离开确认)**,不改自动保存(API Key/半份 cmhub 配置不宜自动写盘)。方案:① 脏追踪——⑤ 所有可编辑控件(cmhub Base URL/API Key/别名下拉、模型详情、生成参数、路径端口、Shopee 更新安全等)接 `textChanged`/`currentIndexChanged`/`toggled`/`valueChanged` 置 `self._dirty=True`;「保存设置」成功后清 `False`;`save_app_settings()` 或等价保存入口必须返回成功/失败,失败时保留 dirty 并阻止离开。② 程序化回填保护——加载配置、保存后重载、刷新别名写回下拉、`_populate_app_settings()` / `_populate_cmhub_alias_combos()` 等必须用 `_suspend_dirty` 或 `QSignalBlocker` 避免误置脏;用户手动输入后由刷新/测试触发的 Base URL 规整仍应保持 dirty,提醒其保存。③ 离开拦截——`main_window` 切 Tab 与 `closeEvent` 若发现⑤ `_dirty` 为真,弹三选一「保存 / 放弃 / 取消」:保存则调保存入口且成功后再切/关;放弃必须重新从 `config.json` + `config/cmhub.json` 载入并回填控件,再清 dirty,不能只清标志;取消则留在⑤不切不关。`QTabWidget.currentChanged` 是切换后信号,实现时需维护上一 Tab index,并用 `_reverting_tab_change`/类似 guard 防止 `setCurrentIndex()` 递归触发。④ 视觉提示——「保存设置」旁显示「● 未保存更改」,保存或放弃回填后消失。⑤ 「刷新别名/测试连接」成功后提示语补「别名已拉取,记得点『保存设置』持久化」,**不**顺带偷偷保存 url/key(保持显式保存一致性)。只改 ⑤ 设置页、`main_window` Tab 切换/关闭守卫和 GUI 单测(脏标志置位/清除、切 Tab 保存/放弃/取消/保存失败分支、程序化回填不置脏、放弃会还原输入、currentChanged 回退不递归);不改配置 schema、AI HTTP 协议、DB schema、Excel、Shopee/CDP 流程 | DONE | | T-531 | ⑤ 设置未保存状态追踪 + 离开确认 | T-530, T-529, T-527 | 现象:在⑤填写 cmhub Base URL/API Key(或改任何设置)后,没有任何"未保存"提示;切换到其它 Tab、关闭窗口都不拦截,输入静默丢失。且「刷新别名/测试连接」用输入框实时文本、不落盘,成功后易误以为已保存。采用**方式 B(显式保存 + 脏状态追踪 + 离开确认)**,不改自动保存(API Key/半份 cmhub 配置不宜自动写盘)。方案:① 脏追踪——⑤ 所有可编辑控件(cmhub Base URL/API Key/别名下拉、模型详情、生成参数、路径端口、蝦皮更新安全等)接 `textChanged`/`currentIndexChanged`/`toggled`/`valueChanged` 置 `self._dirty=True`;「保存设置」成功后清 `False`;`save_app_settings()` 或等价保存入口必须返回成功/失败,失败时保留 dirty 并阻止离开。② 程序化回填保护——加载配置、保存后重载、刷新别名写回下拉、`_populate_app_settings()` / `_populate_cmhub_alias_combos()` 等必须用 `_suspend_dirty` 或 `QSignalBlocker` 避免误置脏;用户手动输入后由刷新/测试触发的 Base URL 规整仍应保持 dirty,提醒其保存。③ 离开拦截——`main_window` 切 Tab 与 `closeEvent` 若发现⑤ `_dirty` 为真,弹三选一「保存 / 放弃 / 取消」:保存则调保存入口且成功后再切/关;放弃必须重新从 `config.json` + `config/cmhub.json` 载入并回填控件,再清 dirty,不能只清标志;取消则留在⑤不切不关。`QTabWidget.currentChanged` 是切换后信号,实现时需维护上一 Tab index,并用 `_reverting_tab_change`/类似 guard 防止 `setCurrentIndex()` 递归触发。④ 视觉提示——「保存设置」旁显示「● 未保存更改」,保存或放弃回填后消失。⑤ 「刷新别名/测试连接」成功后提示语补「别名已拉取,记得点『保存设置』持久化」,**不**顺带偷偷保存 url/key(保持显式保存一致性)。只改 ⑤ 设置页、`main_window` Tab 切换/关闭守卫和 GUI 单测(脏标志置位/清除、切 Tab 保存/放弃/取消/保存失败分支、程序化回填不置脏、放弃会还原输入、currentChanged 回退不递归);不改配置 schema、AI HTTP 协议、DB schema、Excel、Shopee/CDP 流程 | DONE |
| T-532 | ⑤ cmhub 连接成功提示显示账号名 | T-531, T-527, T-528 | 问题:⑤「测试连接/查余额」或刷新别名成功后,界面只提示 `cmhub 连接成功:...`,用户无法确认当前 API Key 实际属于哪个 cmhub 账号,尤其多账号或换 Key 时容易误判。方案:成功 payload 中若 `/api/v1/balance` 返回可识别账号信息,则在结果 label 和状态栏优先显示 `cmhub 账号「<账号名>」连接成功:生文别名 X 个,生图别名 X 个;余额 Y;别名已拉取,记得点『保存设置』持久化`;当前接口结构按 `{ "user": "cmhub_user", "points_balance": 88, "account": { "username": "cmhub_user", "display_name": "主账号" } }` 兼容,显示名优先取 `account.display_name/name/account_name/username/email/id`,再兜底 `user` 字符串或 `user.name/account_name/username/email/id`、顶层 `display_name/name/username/email/id`;邮箱需要脱敏展示;没有账号字段、仅刷新别名不查余额或接口未返回用户信息时,保留现有 `cmhub 连接成功:...` 文案。不得把 API Key、token、完整邮箱或原始敏感响应写入 GUI、run log、诊断日志;不改变 cmhub HTTP 协议、配置 schema、保存逻辑、AI 生成流程或 Shopee/CDP 流程。同步 GUI 单测覆盖有账号名、当前接口结构、只有邮箱、无账号名四种成功提示 | DONE | | T-532 | ⑤ cmhub 连接成功提示显示账号名 | T-531, T-527, T-528 | 问题:⑤「测试连接/查余额」或刷新别名成功后,界面只提示 `cmhub 连接成功:...`,用户无法确认当前 API Key 实际属于哪个 cmhub 账号,尤其多账号或换 Key 时容易误判。方案:成功 payload 中若 `/api/v1/balance` 返回可识别账号信息,则在结果 label 和状态栏优先显示 `cmhub 账号「<账号名>」连接成功:生文别名 X 个,生图别名 X 个;余额 Y;别名已拉取,记得点『保存设置』持久化`;当前接口结构按 `{ "user": "cmhub_user", "points_balance": 88, "account": { "username": "cmhub_user", "display_name": "主账号" } }` 兼容,显示名优先取 `account.display_name/name/account_name/username/email/id`,再兜底 `user` 字符串或 `user.name/account_name/username/email/id`、顶层 `display_name/name/username/email/id`;邮箱需要脱敏展示;没有账号字段、仅刷新别名不查余额或接口未返回用户信息时,保留现有 `cmhub 连接成功:...` 文案。不得把 API Key、token、完整邮箱或原始敏感响应写入 GUI、run log、诊断日志;不改变 cmhub HTTP 协议、配置 schema、保存逻辑、AI 生成流程或 Shopee/CDP 流程。同步 GUI 单测覆盖有账号名、当前接口结构、只有邮箱、无账号名四种成功提示 | DONE |
| T-533 | ② 增量生成:按缺失组件补生成(先标题后封面分离) | T-303, T-520, T-509 | 现象:先关闭「生成封面图片」只生成了标题(`stage=generated/status=success`、`new_cover_path` 为空),之后勾选封面再点「开始生成」,提示「当前筛选结果没有待生成任务」,无法补生成封面;若改用「重置生成结果」会连标题一起清掉、重生成时白扣一次标题点并覆盖 T-509 手动改的标题。根因:`ai.is_generatable_task` 把 `generated+success` 一律判为不可生成,且 `generate_batch` 对每条 eligible 任务都先重生成标题再生成封面。方案:把「开始生成」改为**按缺失组件增量补齐**——① 判定放宽:`generated+success` 且(封面开关 ON 且 `new_cover_path` 为空)判为可生成(只补封面);② `generate_batch` 组件级跳过:`new_title` 已存在则**跳过标题生成**(复用现有标题,不再调 cmhub 生文、不扣标题点、**不覆盖用户手动标题**),封面开关 ON 且 `new_cover_path` 为空才生成封面;③ 进度计数分组件——`title_total`=缺标题任务数、`cover_total`=(封面开关 ON 时)缺封面任务数,不再用 `len(eligible)` 一刀切;④「没有待生成任务」提示只在**按当前设置真的无缺口**(标题全有 且(封面关 或 封面全有))时出现;⑤ 幂等/点数:全组件齐备的任务不纳入、不重复调用扣点。保持 `set_generated` 写库、run_logs、诊断日志、停止取消未开始项语义不变;不改 cmhub HTTP 协议、DB schema、Excel、Shopee/CDP 流程。单测覆盖:只补封面(跳过标题、标题点不增、手动标题不被覆盖)、混合批(部分缺标题+部分缺封面)、无缺口提示无任务、封面关时标题态任务不纳入 | DONE | | T-533 | ② 增量生成:按缺失组件补生成(先标题后封面分离) | T-303, T-520, T-509 | 现象:先关闭「生成封面图片」只生成了标题(`stage=generated/status=success`、`new_cover_path` 为空),之后勾选封面再点「开始生成」,提示「当前筛选结果没有待生成任务」,无法补生成封面;若改用「重置生成结果」会连标题一起清掉、重生成时白扣一次标题点并覆盖 T-509 手动改的标题。根因:`ai.is_generatable_task` 把 `generated+success` 一律判为不可生成,且 `generate_batch` 对每条 eligible 任务都先重生成标题再生成封面。方案:把「开始生成」改为**按缺失组件增量补齐**——① 判定放宽:`generated+success` 且(封面开关 ON 且 `new_cover_path` 为空)判为可生成(只补封面);② `generate_batch` 组件级跳过:`new_title` 已存在则**跳过标题生成**(复用现有标题,不再调 cmhub 生文、不扣标题点、**不覆盖用户手动标题**),封面开关 ON 且 `new_cover_path` 为空才生成封面;③ 进度计数分组件——`title_total`=缺标题任务数、`cover_total`=(封面开关 ON 时)缺封面任务数,不再用 `len(eligible)` 一刀切;④「没有待生成任务」提示只在**按当前设置真的无缺口**(标题全有 且(封面关 或 封面全有))时出现;⑤ 幂等/点数:全组件齐备的任务不纳入、不重复调用扣点。保持 `set_generated` 写库、run_logs、诊断日志、停止取消未开始项语义不变;不改 cmhub HTTP 协议、DB schema、Excel、Shopee/CDP 流程。单测覆盖:只补封面(跳过标题、标题点不增、手动标题不被覆盖)、混合批(部分缺标题+部分缺封面)、无缺口提示无任务、封面关时标题态任务不纳入 | DONE |
| T-534 | ② 重置增强:多选/筛选范围 + 按组件(标题/封面/全部)重置 | T-533, T-404a, T-509 | 现状:②「重置生成结果」只能对单条、且整条全清(`db.reset_generated` 同时清 `new_title` 和 `new_cover_path` 退回 collected)。真实诉求是精准重置——一批标题不满意但封面 OK(只重标题、保封面、不重扣封面点)、或封面差但标题 OK(只重封面、**保留 `new_title` 保护 T-509 手动标题**、不重扣标题点);且需要作用于多条。方案:与 T-533 的组件模型咬合——① 后端 `db.reset_generated` 加组件开关 `reset_title`/`reset_cover`(`delete_file` 仅在重置封面时可选),只清对应字段与 `last_error`,stage 保持 `generated`,由 T-533 泛化后的 `is_generatable_task`(缺任一必需组件即可生成)识别缺口后由「开始生成」只补被清组件;② UI 重置入口给三选项「重置标题 / 重置封面 / 重置全部」;③ 作用域从单条扩到**多选(表格改 `ExtendedSelection`)或当前筛选结果**,与③「作用于当前筛选结果」心智一致;④ 二次确认框显示将重置的标题/封面条数、其中 `committed=1` 条数,并提示「线上已提交、本地重置不回滚 Shopee、重生成后再更新会再次提交线上」+ 重生成会消耗 cmhub 点数;⑤ 运行中禁用;批量写 `run_type=reset` 运行日志,不触碰 Shopee、不回写 Excel。**只重置封面时绝不改 `new_title`**。不改 cmhub HTTP 协议、Excel、Shopee/CDP 流程。单测覆盖:只重标题保留封面、只重封面保留标题(含手动标题不被清)、重置全部、多选/筛选批量、committed 提示、运行中禁用 | DONE | | T-534 | ② 重置增强:多选/筛选范围 + 按组件(标题/封面/全部)重置 | T-533, T-404a, T-509 | 现状:②「重置生成结果」只能对单条、且整条全清(`db.reset_generated` 同时清 `new_title` 和 `new_cover_path` 退回 collected)。真实诉求是精准重置——一批标题不满意但封面 OK(只重标题、保封面、不重扣封面点)、或封面差但标题 OK(只重封面、**保留 `new_title` 保护 T-509 手动标题**、不重扣标题点);且需要作用于多条。方案:与 T-533 的组件模型咬合——① 后端 `db.reset_generated` 加组件开关 `reset_title`/`reset_cover`(`delete_file` 仅在重置封面时可选),只清对应字段与 `last_error`,stage 保持 `generated`,由 T-533 泛化后的 `is_generatable_task`(缺任一必需组件即可生成)识别缺口后由「开始生成」只补被清组件;② UI 重置入口给三选项「重置标题 / 重置封面 / 重置全部」;③ 作用域从单条扩到**多选(表格改 `ExtendedSelection`)或当前筛选结果**,与③「作用于当前筛选结果」心智一致;④ 二次确认框显示将重置的标题/封面条数、其中 `committed=1` 条数,并提示「线上已提交、本地重置不回滚蝦皮、重生成后再更新会再次提交线上」+ 重生成会消耗 cmhub 点数;⑤ 运行中禁用;批量写 `run_type=reset` 运行日志,不触碰蝦皮、不回写 Excel。**只重置封面时绝不改 `new_title`**。不改 cmhub HTTP 协议、Excel、Shopee/CDP 流程。单测覆盖:只重标题保留封面、只重封面保留标题(含手动标题不被清)、重置全部、多选/筛选批量、committed 提示、运行中禁用 | DONE |
| T-535 | ② cmhub 生成标题读取等待固定 600 秒 | T-526, T-533 | 问题:当前 cmhub 生文请求的读取等待时间复用 `ai.resolution_timeouts`,会跟随当前分辨率变化;默认 `1k=240s`,若上游模型排队或响应较慢,标题生成容易先超时。方案:cmhub `gen_title()` 的 `title_request` 读取等待固定使用 600 秒(等同 4k 上限),不再跟随当前分辨率;连接超时仍使用 `ai.cmhub.connect_timeout`,重试次数仍使用 `ai.retry`。封面生成与图片下载继续按分辨率读取等待,direct 兼容路径保持现状。补 `tests/test_ai.py` 断言标题请求 timeout 为 `(connect_timeout, 600)`,不改配置 schema、cmhub HTTP 协议、DB、Excel、Shopee/CDP 或 GUI 交互 | DONE | | T-535 | ② cmhub 生成标题读取等待固定 600 秒 | T-526, T-533 | 问题:当前 cmhub 生文请求的读取等待时间复用 `ai.resolution_timeouts`,会跟随当前分辨率变化;默认 `1k=240s`,若上游模型排队或响应较慢,标题生成容易先超时。方案:cmhub `gen_title()` 的 `title_request` 读取等待固定使用 600 秒(等同 4k 上限),不再跟随当前分辨率;连接超时仍使用 `ai.cmhub.connect_timeout`,重试次数仍使用 `ai.retry`。封面生成与图片下载继续按分辨率读取等待,direct 兼容路径保持现状。补 `tests/test_ai.py` 断言标题请求 timeout 为 `(connect_timeout, 600)`,不改配置 schema、cmhub HTTP 协议、DB、Excel、Shopee/CDP 或 GUI 交互 | DONE |
| T-536 | GUI 按钮圆角全局统一 | T-512, T-513, T-523 | 现象:只有③「开始更新」及少数上色按钮(删除批次/删除账号/未匹配(n))有 `border-radius: 4px`,其余按钮走原生渲染显直角——不一致。根因:圆角是 T-512/T-513 给按钮上色时顺带写进 QSS 的副产品,不是全局形状决策;一旦给 `QPushButton` 设 stylesheet 就放弃原生渲染,才补了 radius/border。方案(**方式 A:全局统一圆角**):① 在 `app/gui/widgets.py` 抽一个**共享按钮基础样式常量/helper**(统一 `border-radius`,如 4px,与卡片 6px 圆角语言协调),并**接管按钮的全部视觉状态**——normal/hover/pressed/disabled/focus 的背景、边框都定义好,避免全局设 QSS 后按钮变扁平方块、丢 hover 反馈;② 在主窗口/app 级用全局 `QPushButton` QSS 应用该基础样式,让**所有按钮共享同一圆角**;③ warning(`startUpdateButton`)与 danger(`_danger_outline_button_style`:删除批次/删除账号/未匹配)按钮改为**只叠加颜色**,复用共享基础样式的圆角/内边距/状态,不再各自重写 radius/border——杜绝“上色=顺带圆角”的隐性耦合;④ 以 Windows 为主目标做一次视觉自测(hover/按下/禁用不劣于原生)。只改 GUI 样式层(`widgets.py` + 主窗口全局 QSS + 各上色按钮引用),不改任何按钮的启用/禁用逻辑、行为、业务流程、DB、Excel、Shopee/CDP。GUI 单测至少断言上色按钮仍带各自语义色且不再各自硬写 radius(改为引用共享样式);圆角外观本身以人工视觉验收为准 | DONE | | T-536 | GUI 按钮圆角全局统一 | T-512, T-513, T-523 | 现象:只有③「开始更新」及少数上色按钮(删除批次/删除账号/未匹配(n))有 `border-radius: 4px`,其余按钮走原生渲染显直角——不一致。根因:圆角是 T-512/T-513 给按钮上色时顺带写进 QSS 的副产品,不是全局形状决策;一旦给 `QPushButton` 设 stylesheet 就放弃原生渲染,才补了 radius/border。方案(**方式 A:全局统一圆角**):① 在 `app/gui/widgets.py` 抽一个**共享按钮基础样式常量/helper**(统一 `border-radius`,如 4px,与卡片 6px 圆角语言协调),并**接管按钮的全部视觉状态**——normal/hover/pressed/disabled/focus 的背景、边框都定义好,避免全局设 QSS 后按钮变扁平方块、丢 hover 反馈;② 在主窗口/app 级用全局 `QPushButton` QSS 应用该基础样式,让**所有按钮共享同一圆角**;③ warning(`startUpdateButton`)与 danger(`_danger_outline_button_style`:删除批次/删除账号/未匹配)按钮改为**只叠加颜色**,复用共享基础样式的圆角/内边距/状态,不再各自重写 radius/border——杜绝“上色=顺带圆角”的隐性耦合;④ 以 Windows 为主目标做一次视觉自测(hover/按下/禁用不劣于原生)。只改 GUI 样式层(`widgets.py` + 主窗口全局 QSS + 各上色按钮引用),不改任何按钮的启用/禁用逻辑、行为、业务流程、DB、Excel、Shopee/CDP。GUI 单测至少断言上色按钮仍带各自语义色且不再各自硬写 radius(改为引用共享样式);圆角外观本身以人工视觉验收为准 | DONE |
@@ -142,6 +142,7 @@
| ID | 任务 | 依赖 | 验收要点 | 状态 | | ID | 任务 | 依赖 | 验收要点 | 状态 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| T-541 | 打包版主窗口初始位置与小屏适配 | T-523, T-537, T-540 | Bug:在 Windows 10 虚拟机/小分辨率环境运行打包后的 `dist/cmshopee/cmshopee.exe` 或 release 包时,主窗口可能贴到屏幕左上角且左边/标题栏显示不全,看不到「蝦皮圈優化助手 v<APP_VERSION>」标题,用户无法拖动窗口到中间。根因:`MainWindow.__init__()` 只调用 `self.resize(1180, 760)`,没有根据 `QApplication.primaryScreen().availableGeometry()` 限制初始尺寸、没有居中、没有保证窗口 frame/title bar 在可用屏幕范围内;PyInstaller 本身不是根因,小屏、高 DPI、任务栏占用、多显示器或 VM 分辨率更容易触发。方案:① 抽一个主窗口初始布局 helper(如 `_fit_and_center_window(window, preferred_size=(1180,760), margin=40)`),读取当前屏幕 `availableGeometry()`;② 初始宽高不超过可用区域减安全边距,保留合理最小值(如 960x640,若屏幕更小则继续适配);③ `resize()` 后按可用区域中心 `move()`,确保 `frameGeometry()` 左上角不小于可用区域左上角,标题栏完整可见;④ offscreen/headless 测试环境不依赖真实屏幕时可跳过或用 mock geometry;⑤ 不保存/恢复历史窗口位置,避免把坏坐标持久化。同步 `docs/packaging.md` 打包验收:在目标 VM/小分辨率环境启动 release exe,窗口标题栏必须完整可见且可拖动。GUI 单测覆盖小屏 mock 下尺寸被限制、窗口被居中/夹进可用区域;不改业务流程、DB、AI、Excel、CDP/Shopee、打包产物结构 | DONE | | T-541 | 打包版主窗口初始位置与小屏适配 | T-523, T-537, T-540 | Bug:在 Windows 10 虚拟机/小分辨率环境运行打包后的 `dist/cmshopee/cmshopee.exe` 或 release 包时,主窗口可能贴到屏幕左上角且左边/标题栏显示不全,看不到「蝦皮圈優化助手 v<APP_VERSION>」标题,用户无法拖动窗口到中间。根因:`MainWindow.__init__()` 只调用 `self.resize(1180, 760)`,没有根据 `QApplication.primaryScreen().availableGeometry()` 限制初始尺寸、没有居中、没有保证窗口 frame/title bar 在可用屏幕范围内;PyInstaller 本身不是根因,小屏、高 DPI、任务栏占用、多显示器或 VM 分辨率更容易触发。方案:① 抽一个主窗口初始布局 helper(如 `_fit_and_center_window(window, preferred_size=(1180,760), margin=40)`),读取当前屏幕 `availableGeometry()`;② 初始宽高不超过可用区域减安全边距,保留合理最小值(如 960x640,若屏幕更小则继续适配);③ `resize()` 后按可用区域中心 `move()`,确保 `frameGeometry()` 左上角不小于可用区域左上角,标题栏完整可见;④ offscreen/headless 测试环境不依赖真实屏幕时可跳过或用 mock geometry;⑤ 不保存/恢复历史窗口位置,避免把坏坐标持久化。同步 `docs/packaging.md` 打包验收:在目标 VM/小分辨率环境启动 release exe,窗口标题栏必须完整可见且可拖动。GUI 单测覆盖小屏 mock 下尺寸被限制、窗口被居中/夹进可用区域;不改业务流程、DB、AI、Excel、CDP/Shopee、打包产物结构 | DONE |
| T-542 | GUI 用户可见 `shopee/Shopee` 文案统一改为「蝦皮」 | T-523, T-537 | 需求:运行程序后,GUI 上用户能看到的 `shopee` / `Shopee` 字符统一改成中文「蝦皮」,例如主 Tab「③ 更新shopee」改为「③ 更新蝦皮」,设置分区「Shopee 更新安全 / 执行模式」改为「蝦皮更新安全 / 执行模式」,引导、弹窗、状态栏、运行日志、空状态、右键/确认文案里的“更新 Shopee / 未打开 Shopee / 不回滚 Shopee”等也改为「蝦皮」。范围:只改**用户可见文案与 GUI 测试断言**,包括 `app/gui/widgets.py` 的 `TAB_TITLES`、`MainWindow` 里依赖 Tab 文案的判断、`tabs/apply.py`、`tabs/settings.py`、`tabs/collect.py`、`tabs/generate.py`、`gui/workers.py` 中的可见提示和相关 `tests/test_gui.py`;同时修正 `app/editor.py` 里会冒到 GUI 的自定义错误文案。边界:不得改内部标识符、配置键、DB 字段、文件名、URL、域名、CDP 选择器和第三方原始错误,例如 `shopee_update`、`cmshopee`、`shopee待处理任务模板.xlsx`、`seller.shopee.tw`、`.shopee-image-manager`、`window.__cmshopee_*`、`cmshopee.db` 必须保持原样;代码注释/技术文档中指平台事实可继续写 `Shopee`。验收:`rg -n "更新shopee|Shopee 更新安全|更新 Shopee|未打开 Shopee|不回滚 Shopee" app tests` 不命中;`python -m compileall app main.py` 与 `python -m unittest discover -s tests` 通过;不涉及 CDP 流程实跑 | DONE |
| 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 作为后续可选。当前因业务优先级调整顺延到 T-526~T-528 之后 | 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 作为后续可选。当前因业务优先级调整顺延到 T-526~T-528 之后 | TODO |
| T-537 | 品牌名「蝦皮圈優化助手」(显示层中文品牌,cmshopee 保留为代号/标识符) | T-523 | 需求:项目/软件正式定名「蝦皮圈優化助手」。**原则**:产品**显示/品牌名** = 「蝦皮圈優化助手」;`cmshopee` 保留为**代码库/仓库/技术代号**,一切**标识符不改**——遵项目「文档与 UI 文案用中文、标识符用英文」规则,且避免非 ASCII 文件名破坏现有用户数据。**改(显示/品牌层)**:① `app/gui/main_window.py` 窗口标题 `setWindowTitle("cmshopee")` → `"蝦皮圈優化助手"`(任务栏/标题栏显示中文品牌);② 文档产品名散文 `docs/00-ai-start-here.md`、`docs/README.md`、`AGENTS.md`(及 vision/brief 若有)把「cmshopee 是一个…」改为「蝦皮圈優化助手(代号 cmshopee)是一个…」,明确品牌名并保留代号;③ 空的 `README.md` 补品牌标题行;④ 可选:`app/gui/__init__.py` PySide6 缺失启动失败提示带上品牌名。**不改(技术标识符,保留 cmshopee)**:`app` 包名、`cmshopee.exe` 打包产物名、`cmshopee.db`(+`-wal/-shm`)/`db_path` 默认值、`cmshopee.spec`、`cmshopee.log`、`config.json`/`config/*.json` 文件名、slug/`chrome_user_data_dir`、仓库目录;`app/editor.py` 注入页面的 JS 全局 `window.__cmshopee_*`(JS 标识符须 ASCII)、`app/ai.py` multipart boundary `----cmshopee-*`、`app/workers.py` `thread._cmshopee_worker`、`app/chrome.py` 快捷方式内部前缀 `cmshopee-`。**决策点**:`app/excel.py` 回写文件名后缀 `_cmshopee回写` 为用户可见产物名——本任务**默认保留**(改动会影响已生成副本命名习惯),如需改「_優化助手回写」另行确认。**影响**:不改 db/exe/spec/包名 → 打包、更新流程、现有用户数据零影响。验收:窗口标题栏/任务栏/README/相关文档显示「蝦皮圈優化助手」;`cmshopee.db`/exe/包名不变;GUI 单测若断言窗口标题须同步更新;`python -m compileall app main.py` + `python -m unittest discover -s tests` 全绿。边界:纯显示/文档层,不改任何文件名、schema、流程、CDP/Shopee 逻辑 | DONE | | T-537 | 品牌名「蝦皮圈優化助手」(显示层中文品牌,cmshopee 保留为代号/标识符) | T-523 | 需求:项目/软件正式定名「蝦皮圈優化助手」。**原则**:产品**显示/品牌名** = 「蝦皮圈優化助手」;`cmshopee` 保留为**代码库/仓库/技术代号**,一切**标识符不改**——遵项目「文档与 UI 文案用中文、标识符用英文」规则,且避免非 ASCII 文件名破坏现有用户数据。**改(显示/品牌层)**:① `app/gui/main_window.py` 窗口标题 `setWindowTitle("cmshopee")` → `"蝦皮圈優化助手"`(任务栏/标题栏显示中文品牌);② 文档产品名散文 `docs/00-ai-start-here.md`、`docs/README.md`、`AGENTS.md`(及 vision/brief 若有)把「cmshopee 是一个…」改为「蝦皮圈優化助手(代号 cmshopee)是一个…」,明确品牌名并保留代号;③ 空的 `README.md` 补品牌标题行;④ 可选:`app/gui/__init__.py` PySide6 缺失启动失败提示带上品牌名。**不改(技术标识符,保留 cmshopee)**:`app` 包名、`cmshopee.exe` 打包产物名、`cmshopee.db`(+`-wal/-shm`)/`db_path` 默认值、`cmshopee.spec`、`cmshopee.log`、`config.json`/`config/*.json` 文件名、slug/`chrome_user_data_dir`、仓库目录;`app/editor.py` 注入页面的 JS 全局 `window.__cmshopee_*`(JS 标识符须 ASCII)、`app/ai.py` multipart boundary `----cmshopee-*`、`app/workers.py` `thread._cmshopee_worker`、`app/chrome.py` 快捷方式内部前缀 `cmshopee-`。**决策点**:`app/excel.py` 回写文件名后缀 `_cmshopee回写` 为用户可见产物名——本任务**默认保留**(改动会影响已生成副本命名习惯),如需改「_優化助手回写」另行确认。**影响**:不改 db/exe/spec/包名 → 打包、更新流程、现有用户数据零影响。验收:窗口标题栏/任务栏/README/相关文档显示「蝦皮圈優化助手」;`cmshopee.db`/exe/包名不变;GUI 单测若断言窗口标题须同步更新;`python -m compileall app main.py` + `python -m unittest discover -s tests` 全绿。边界:纯显示/文档层,不改任何文件名、schema、流程、CDP/Shopee 逻辑 | DONE |
| T-538 | 打包产物用户数据收进 `data/` 子文件夹(便携方案 1) | T-524 | 问题:当前 onedir 产物把用户数据(`config.json`、`config/ai_models.json`、`config/cmhub.json`、`cmshopee.db(+-wal/-shm)`、`chrome_user_data_dir/`、`images/`、`logs/`、`prompts/`、`title_prompt.txt`)生成在 exe 顶层,和程序文件/依赖文件混放:更新易误删数据、放进 Program Files 只读目录会写失败/被 VirtualStore 重定向、程序与数据关注点混杂。方案(保持便携):① **统一数据根**——所有用户数据落到 exe 同级的单个 `data/` 子文件夹;`app/appconfig.py` 用显式 `data_dir`(默认 `<exe目录>/data`,源码运行时可为项目根/`data`)解析并返回绝对路径,**替代现在 `main.py` 的 chdir**(chdir 影响全进程相对路径/文件对话框默认目录,脆弱);文件名保持不变(仍是 `cmshopee.db` 等),只是移入 `data/`。② **旧布局迁移**——新版首次运行若在 exe 顶层发现旧的 `config.json`/`cmshopee.db` 等,自动迁移进 `data/`(或明确提示手动迁移),不得对老用户生成空 `data/` 丢失原数据。③ **可写性检测**——启动时若 `data_dir` 不可写(如放在 Program Files),弹清晰错误「请把程序放到可写目录,勿放 Program Files」并优雅退出,不静默失败。④ **发布只含程序**——`cmshopee.spec`/`scripts/build_exe.ps1` 构建检查确保不打包 `data/` 或任何用户数据;发布 zip 不含 `data/`,从而「整包解压覆盖」也安全(覆盖程序文件、不碰 `data/`);T-540 后当前打包脚本固定 PyInstaller 6.11.1,产物为 `cmshopee.exe` + `_internal/` onedir,具体版本和 release 命名由 T-540 维护。⑤ **更新流程前提**——原地更新=换程序留 `data/`,依赖:DB 迁移幂等且只向前(现 `init_db`+`ALTER TABLE ADD COLUMN`)、`config` 向前兼容(`load_config` 补默认值)。同步更新 `docs/packaging.md`(目录结构、首次运行数据位置、§5 更新步骤简化为「覆盖新版程序文件和依赖文件,`data/` 不动」)。测试:`test_appconfig` 断言各路径解析进 `data_dir`、可写性检测行为、旧布局迁移;`test_packaging` 断言产物不含 `data/`/用户数据。边界:只改路径解析层(`appconfig` 路径 + `main.py` 数据根 + 迁移/可写检测)、spec/构建脚本、打包文档;不改文件名、DB schema、业务逻辑、AI/Excel、CDP/Shopee 流程 | DONE | | T-538 | 打包产物用户数据收进 `data/` 子文件夹(便携方案 1) | T-524 | 问题:当前 onedir 产物把用户数据(`config.json`、`config/ai_models.json`、`config/cmhub.json`、`cmshopee.db(+-wal/-shm)`、`chrome_user_data_dir/`、`images/`、`logs/`、`prompts/`、`title_prompt.txt`)生成在 exe 顶层,和程序文件/依赖文件混放:更新易误删数据、放进 Program Files 只读目录会写失败/被 VirtualStore 重定向、程序与数据关注点混杂。方案(保持便携):① **统一数据根**——所有用户数据落到 exe 同级的单个 `data/` 子文件夹;`app/appconfig.py` 用显式 `data_dir`(默认 `<exe目录>/data`,源码运行时可为项目根/`data`)解析并返回绝对路径,**替代现在 `main.py` 的 chdir**(chdir 影响全进程相对路径/文件对话框默认目录,脆弱);文件名保持不变(仍是 `cmshopee.db` 等),只是移入 `data/`。② **旧布局迁移**——新版首次运行若在 exe 顶层发现旧的 `config.json`/`cmshopee.db` 等,自动迁移进 `data/`(或明确提示手动迁移),不得对老用户生成空 `data/` 丢失原数据。③ **可写性检测**——启动时若 `data_dir` 不可写(如放在 Program Files),弹清晰错误「请把程序放到可写目录,勿放 Program Files」并优雅退出,不静默失败。④ **发布只含程序**——`cmshopee.spec`/`scripts/build_exe.ps1` 构建检查确保不打包 `data/` 或任何用户数据;发布 zip 不含 `data/`,从而「整包解压覆盖」也安全(覆盖程序文件、不碰 `data/`);T-540 后当前打包脚本固定 PyInstaller 6.11.1,产物为 `cmshopee.exe` + `_internal/` onedir,具体版本和 release 命名由 T-540 维护。⑤ **更新流程前提**——原地更新=换程序留 `data/`,依赖:DB 迁移幂等且只向前(现 `init_db`+`ALTER TABLE ADD COLUMN`)、`config` 向前兼容(`load_config` 补默认值)。同步更新 `docs/packaging.md`(目录结构、首次运行数据位置、§5 更新步骤简化为「覆盖新版程序文件和依赖文件,`data/` 不动」)。测试:`test_appconfig` 断言各路径解析进 `data_dir`、可写性检测行为、旧布局迁移;`test_packaging` 断言产物不含 `data/`/用户数据。边界:只改路径解析层(`appconfig` 路径 + `main.py` 数据根 + 迁移/可写检测)、spec/构建脚本、打包文档;不改文件名、DB schema、业务逻辑、AI/Excel、CDP/Shopee 流程 | DONE |
@@ -154,7 +155,7 @@
- M2:账号管理可用、能登录与检测(Phase 1)。 - M2:账号管理可用、能登录与检测(Phase 1)。
- M3:导入 + 采集 + 回写旧数据(Phase 2)。 - M3:导入 + 采集 + 回写旧数据(Phase 2)。
- M4:AI 生成新标题/新封面(Phase 3)。 - M4:AI 生成新标题/新封面(Phase 3)。
- M5:对已生成任务弹窗批量确认后更新 Shopee + 回写结果(Phase 4);真实线上单条冒烟延后到设置安全开关完成后执行。 - M5:对已生成任务弹窗批量确认后更新蝦皮 + 回写结果(Phase 4);真实线上单条冒烟延后到设置安全开关完成后执行。
## 待办池(Backlog) ## 待办池(Backlog)
+7 -7
View File
@@ -217,7 +217,7 @@ login_status_text(status) -> str
- `debug_port` 在账号服务层按账号唯一校验;默认端口取 `debug_port_range` 中第一个未占用端口。 - `debug_port` 在账号服务层按账号唯一校验;默认端口取 `debug_port_range` 中第一个未占用端口。
- `delete_account()` 只删除 DB 账号记录,不删除本地 user-data-dir,避免误删登录态。 - `delete_account()` 只删除 DB 账号记录,不删除本地 user-data-dir,避免误删登录态。
- `launch_for_login()` 是幂等入口。先检查 `chrome.is_running(account.debug_port)`;若该账号 CDP 端口已响应,不再调用 `subprocess.Popen`,而是复用已打开的账号 Chrome,优先激活/打开 `https://<region_host>/portal/` 或卖家中心登录 tab,并返回 `reused=true`;若端口未响应,才启动带该账号 user-data-dir 和 CDP 端口的 Chrome,等待 CDP 就绪并返回 `launched=true`。不会读取、填写或提交密码,不绕过验证码。 - `launch_for_login()` 是幂等入口。先检查 `chrome.is_running(account.debug_port)`;若该账号 CDP 端口已响应,不再调用 `subprocess.Popen`,而是复用已打开的账号 Chrome,优先激活/打开 `https://<region_host>/portal/` 或卖家中心登录 tab,并返回 `reused=true`;若端口未响应,才启动带该账号 user-data-dir 和 CDP 端口的 Chrome,等待 CDP 就绪并返回 `launched=true`。不会读取、填写或提交密码,不绕过验证码。
- ③ 更新 shopee 的账号预检不会自动调用 `launch_for_login()`;Chrome 未启动/端口不可达时只返回阻断原因,由 GUI 提示用户去④手动打开账号浏览器并登录。T-105b 的复用逻辑只作用于④用户主动点击「启动登录」这一入口。 - ③ 更新蝦皮的账号预检不会自动调用 `launch_for_login()`;Chrome 未启动/端口不可达时只返回阻断原因,由 GUI 提示用户去④手动打开账号浏览器并登录。T-105b 的复用逻辑只作用于④用户主动点击「启动登录」这一入口。
- `create_shortcut()` 生成 `.lnk`,目标/参数复用 `chrome.build_launch_args()`,不包含密码。 - `create_shortcut()` 生成 `.lnk`,目标/参数复用 `chrome.build_launch_args()`,不包含密码。
- `detect_login()` 复用 `editor.login_status()`;检测为已登录时更新 `last_login_at`。 - `detect_login()` 复用 `editor.login_status()`;检测为已登录时更新 `last_login_at`。
@@ -354,8 +354,8 @@ main() -> int # 创建 QApplication + MainWindow
class MainWindow(QMainWindow) # QTabWidget: ①②③④⑤;支持注入 db_path/config/config_path/ai_models_path 便于测试 class MainWindow(QMainWindow) # QTabWidget: ①②③④⑤;支持注入 db_path/config/config_path/ai_models_path 便于测试
class CollectTab(QWidget) # ① 导入采集:导入 Excel + 汇总栏 + QTableView 任务列表 + 未匹配略过标记 class CollectTab(QWidget) # ① 导入采集:导入 Excel + 汇总栏 + QTableView 任务列表 + 未匹配略过标记
class GenerateTab(QWidget) # ② AI生成:提示词管理 + 筛选任务 + 生成封面图片成本开关 + 开始/停止生成 + 新旧封面预览 + AI生成运行日志 class GenerateTab(QWidget) # ② AI生成:提示词管理 + 筛选任务 + 生成封面图片成本开关 + 开始/停止生成 + 新旧封面预览 + AI生成运行日志
class ApplyTab(QWidget) # ③ 更新shopee:筛选已生成任务 + 检查本轮更新 + 安全开关拦截 + 确认后分批真实更新 + 运行日志 class ApplyTab(QWidget) # ③ 更新蝦皮:筛选已生成任务 + 检查本轮更新 + 安全开关拦截 + 确认后分批真实更新 + 运行日志
class SettingsTab(QWidget) # ⑤ 设置:cmhub 网关配置 + 响应式三列布局 + 角色/生成参数/路径端口 + Shopee 更新安全 + 未保存状态追踪 class SettingsTab(QWidget) # ⑤ 设置:cmhub 网关配置 + 响应式三列布局 + 角色/生成参数/路径端口 + 蝦皮更新安全 + 未保存状态追踪
class CollectWorker(BaseWorker) # ① 后台采集:账号就绪预检 -> editor.collect -> db.set_collected/mark_skipped/mark_failed class CollectWorker(BaseWorker) # ① 后台采集:账号就绪预检 -> editor.collect -> db.set_collected/mark_skipped/mark_failed
class GenerateWorker(BaseWorker) # ② 后台生成:ai.generate_batch -> db.set_generated/mark_failed + 进度 class GenerateWorker(BaseWorker) # ② 后台生成:ai.generate_batch -> db.set_generated/mark_failed + 进度
class ApplyWorker(BaseWorker) # ③ 后台更新:账号就绪预检 -> 检查或按批调用 editor.apply_task(close_success_tab=...) -> db.set_applied/mark_skipped class ApplyWorker(BaseWorker) # ③ 后台更新:账号就绪预检 -> 检查或按批调用 editor.apply_task(close_success_tab=...) -> db.set_applied/mark_skipped
@@ -372,7 +372,7 @@ TAB_STYLE: str # 顶层 Tab 栏防误点样式:
T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负责旧导入路径兼容与 `main()`;`main_window.py` 放 `MainWindow`;`models.py` 放 3 个 TableModel;`widgets.py` 放色板、空状态卡、批次总览和日志 helper;`workers.py` 放具体 GUI worker;`tabs/` 下按 ①~⑤ 拆分各 Tab。对外仍保留 `from app import gui`、`from app.gui import MainWindow/CollectTab/GenerateWorker/...`。 T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负责旧导入路径兼容与 `main()`;`main_window.py` 放 `MainWindow`;`models.py` 放 3 个 TableModel;`widgets.py` 放色板、空状态卡、批次总览和日志 helper;`workers.py` 放具体 GUI worker;`tabs/` 下按 ①~⑤ 拆分各 Tab。对外仍保留 `from app import gui`、`from app.gui import MainWindow/CollectTab/GenerateWorker/...`。
`MainWindow` 已实现五 Tab、① 导入采集任务列表、② AI生成布局/提示词/开始生成/停止/封面对照预览、③ 更新shopee筛选列表与检查/确认后分批真实更新、④ 账号管理、⑤ AI 模型管理。缺 PySide6 时 `main()` 返回 1 并输出明确提示。 `MainWindow` 已实现五 Tab、① 导入采集任务列表、② AI生成布局/提示词/开始生成/停止/封面对照预览、③ 更新蝦皮筛选列表与检查/确认后分批真实更新、④ 账号管理、⑤ AI 模型管理。缺 PySide6 时 `main()` 返回 1 并输出明确提示。
主 Tab 栏必须在 `MainWindow` 初始化时应用 `TAB_STYLE`:5 个 Tab 不使用 Qt 默认紧凑宽度,需保证点击区域稳定、间距清晰、当前 Tab 高亮明显。该样式属于全局导航基础,不归后续业务 Tab 任务重复实现。 主 Tab 栏必须在 `MainWindow` 初始化时应用 `TAB_STYLE`:5 个 Tab 不使用 Qt 默认紧凑宽度,需保证点击区域稳定、间距清晰、当前 Tab 高亮明显。该样式属于全局导航基础,不归后续业务 Tab 任务重复实现。
@@ -396,7 +396,7 @@ T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负
- 角色与生成参数读写 `config.json`,并按 3 个组件一组排列:标题大模型(仅 text)、图片大模型(仅 image)、标题/图片并发、失败重试、分辨率、jpg 质量。 - 角色与生成参数读写 `config.json`,并按 3 个组件一组排列:标题大模型(仅 text)、图片大模型(仅 image)、标题/图片并发、失败重试、分辨率、jpg 质量。
- 分辨率下拉固定 `512/1k/2k/4k`;返回超时标签只读展示 `resolution_timeouts[resolution]`。 - 分辨率下拉固定 `512/1k/2k/4k`;返回超时标签只读展示 `resolution_timeouts[resolution]`。
- 路径与端口读写 `config.json`,并按 3 个组件一组排列:默认调试端口、调试端口范围、CDP 就绪超时等短字段一格;Chrome 路径、账号数据根目录、图片目录、DB 路径等长字段跨整行或跨 2/3 列。保存时校验端口范围和默认端口。 - 路径与端口读写 `config.json`,并按 3 个组件一组排列:默认调试端口、调试端口范围、CDP 就绪超时等短字段一格;Chrome 路径、账号数据根目录、图片目录、DB 路径等长字段跨整行或跨 2/3 列。保存时校验端口范围和默认端口。
- Shopee 更新安全读写 `config.json` 的 `shopee_update` 段,并按 3 个组件一组排列:允许真实提交、允许更新封面、每批最大更新条数、成功后关闭本次新开编辑页、多账号并行、最大并行账号数;其中「多账号并行更新」与「最大并行账号数」必须合并为同一个横向组件,最大并行账号数紧跟在多账号并行更新后面,不允许被三列表单排到下一行。`dry_run` 字段可保留为内部兼容,但普通用户界面不再展示 dry-run 开关,③ 使用「检查本轮更新」按钮触发检查模式;测试商品 ID 仅作为历史/调试兼容字段保留,不参与普通正式更新安全检查,普通设置页已隐藏该入口。 - 蝦皮更新安全读写 `config.json` 的 `shopee_update` 段,并按 3 个组件一组排列:允许真实提交、允许更新封面、每批最大更新条数、成功后关闭本次新开编辑页、多账号并行、最大并行账号数;其中「多账号并行更新」与「最大并行账号数」必须合并为同一个横向组件,最大并行账号数紧跟在多账号并行更新后面,不允许被三列表单排到下一行。`dry_run` 字段可保留为内部兼容,但普通用户界面不再展示 dry-run 开关,③ 使用「检查本轮更新」按钮触发检查模式;测试商品 ID 仅作为历史/调试兼容字段保留,不参与普通正式更新安全检查,普通设置页已隐藏该入口。
- 真实提交与封面更新默认关闭;用户在 ⑤ 保存开启后,③ 仍需要通过安全开关检查并弹窗确认,才会创建更新 worker。 - 真实提交与封面更新默认关闭;用户在 ⑤ 保存开启后,③ 仍需要通过安全开关检查并弹窗确认,才会创建更新 worker。
① 导入采集当前要点(T-202/T-202b): ① 导入采集当前要点(T-202/T-202b):
@@ -429,7 +429,7 @@ T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负
- 进度标签显示标题/图片/失败;未勾选生成封面时图片进度显示本轮未生成,标题成功即 `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 刷新任务表。 - 进度标签显示标题/图片/失败;未勾选生成封面时图片进度显示本轮未生成,标题成功即 `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` 事件。 - T-404a 已实现:「重置生成结果」读取当前选中单条;确认后调用 DB reset,清空本地 AI 结果并退回可生成状态;默认不删除本地新封面文件;写 `run_type=reset` 事件。
③ 更新shopee当前要点(T-303b/T-401/T-402/T-403): ③ 更新蝦皮当前要点(T-303b/T-401/T-402/T-403):
- `ApplyTab` 顶部筛选栏包含:批次、店铺、商品ID、状态、刷新。批次来自 `db.list_batches()`;店铺来自当前更新候选任务别名并优先显示匹配账号名;商品ID输入框按包含匹配 `item_id`,清空表示全部。 - `ApplyTab` 顶部筛选栏包含:批次、店铺、商品ID、状态、刷新。批次来自 `db.list_batches()`;店铺来自当前更新候选任务别名并优先显示匹配账号名;商品ID输入框按包含匹配 `item_id`,清空表示全部。
- ③ 只列出已具备新标题/新封面、可进入更新阶段的任务:`stage=generated/applied`,或已有新字段且 `status=failed/skipped` 的任务。 - ③ 只列出已具备新标题/新封面、可进入更新阶段的任务:`stage=generated/applied`,或已有新字段且 `status=failed/skipped` 的任务。
@@ -448,7 +448,7 @@ T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负
- 检查和真实更新都会创建 `run_logs`,并把逐条事件写入 `run_log_events`;点击「检查本轮更新」或「开始更新」时先清空 `ApplyTab` 可见日志文本并写入本轮开始摘要,后续只追加本轮日志;③ 页面不自动把上一轮历史日志混入当前运行界面。 - 检查和真实更新都会创建 `run_logs`,并把逐条事件写入 `run_log_events`;点击「检查本轮更新」或「开始更新」时先清空 `ApplyTab` 可见日志文本并写入本轮开始摘要,后续只追加本轮日志;③ 页面不自动把上一轮历史日志混入当前运行界面。
- 若 `close_success_tab=true`,`editor.apply_task()` 只关闭本轮自动新开且成功提交的商品页;确认后跳回商品列表页时,关闭前等待 2 秒;进入编辑页后的失败和复用的用户已有 tab 不关闭。`open_product()` 内部打开失败的新建 tab 由 `open_product()` 自行关闭。 - 若 `close_success_tab=true`,`editor.apply_task()` 只关闭本轮自动新开且成功提交的商品页;确认后跳回商品列表页时,关闭前等待 2 秒;进入编辑页后的失败和复用的用户已有 tab 不关闭。`open_product()` 内部打开失败的新建 tab 由 `open_product()` 自行关闭。
- 别名未匹配账号的任务逐条 `db.mark_skipped()`,原因 `别名未匹配账号`;「停止」调用 worker 协作式 `cancel()`,已开始单条跑到安全边界后结束。 - 别名未匹配账号的任务逐条 `db.mark_skipped()`,原因 `别名未匹配账号`;「停止」调用 worker 协作式 `cancel()`,已开始单条跑到安全边界后结束。
- T-404a/T-508 已实现:「重置更新状态」从底部批处理按钮移到任务表右键菜单,读取当前选中单条,运行中禁用;确认后保留 `new_title/new_cover_path`,本地退回 `stage=generated/status=pending` 供重复更新;`committed=1` 时必须提示线上已提交过且不回滚 Shopee,并保留 committed 历史事实/运行日志。 - T-404a/T-508 已实现:「重置更新状态」从底部批处理按钮移到任务表右键菜单,读取当前选中单条,运行中禁用;确认后保留 `new_title/new_cover_path`,本地退回 `stage=generated/status=pending` 供重复更新;`committed=1` 时必须提示线上已提交过且不回滚蝦皮,并保留 committed 历史事实/运行日志。
- ③ 没有常驻提交开关;确认弹窗是提交线上前的边界。 - ③ 没有常驻提交开关;确认弹窗是提交线上前的边界。
- 更新完成后自动调用 `WriteBackWorker(mode="results")` → `excel.write_back_results()`,把新标题、新封面路径、更新状态写回原 Excel;原文件被占用时提示关闭后点击「回写结果到 Excel」手动重试,SQLite 更新结果不回滚。 - 更新完成后自动调用 `WriteBackWorker(mode="results")` → `excel.write_back_results()`,把新标题、新封面路径、更新状态写回原 Excel;原文件被占用时提示关闭后点击「回写结果到 Excel」手动重试,SQLite 更新结果不回滚。
- 自动结果回写完成后弹窗汇总成功/失败/略过数量与 Excel 回写文件/行数;若没有可回写批次,也会弹出更新汇总。 - 自动结果回写完成后弹窗汇总成功/失败/略过数量与 Excel 回写文件/行数;若没有可回写批次,也会弹出更新汇总。
+20 -18
View File
File diff suppressed because one or more lines are too long
+15 -15
View File
@@ -5,18 +5,18 @@
## Tab 顺序与职责(工作流优先) ## Tab 顺序与职责(工作流优先)
``` ```
① 导入采集 │ ② AI生成 │ ③ 更新shopee │ ④ 账号管理 │ ⑤ 设置 ① 导入采集 │ ② AI生成 │ ③ 更新蝦皮 │ ④ 账号管理 │ ⑤ 设置
``` ```
| Tab | 职责 | 风险 | | Tab | 职责 | 风险 |
| --- | --- | --- | | --- | --- | --- |
| ① 导入采集 | 导入多个 Excel;任务列表;**采集**商品当前的旧标题/旧封面(只读),封面图下载本地;回写 Excel 旧字段 | 只读,低 | | ① 导入采集 | 导入多个 Excel;任务列表;**采集**商品当前的旧标题/旧封面(只读),封面图下载本地;回写 Excel 旧字段 | 只读,低 |
| ② AI生成 | 左侧标题/封面**提示词**;右侧按批次/店铺/商品ID/状态筛选任务列表;AI 生成新标题,并按本轮开关可选生成新封面;已生成任务可本地微调新标题;双击看新旧封面 | 不触线上,中 | | ② AI生成 | 左侧标题/封面**提示词**;右侧按批次/店铺/商品ID/状态筛选任务列表;AI 生成新标题,并按本轮开关可选生成新封面;已生成任务可本地微调新标题;双击看新旧封面 | 不触线上,中 |
| ③ 更新shopee | 对**已生成**任务点击「开始更新」后弹窗确认;确认后打开编辑页换标题+封面并逐条点「更新」提交;结果回写 Excel | **写线上,高** | | ③ 更新蝦皮 | 对**已生成**任务点击「开始更新」后弹窗确认;确认后打开编辑页换标题+封面并逐条点「更新」提交;结果回写 Excel | **写线上,高** |
| ④ 账号管理 | Shopee 账号(账号名/别名/数据目录/端口/密码本地明文仅参考/登录状态);启动登录、检测登录、生成快捷方式;启动登录必须复用已打开的同账号 Chrome,避免重复开窗口;检测登录遇到 `accounts.shopee.tw/seller/login` 必须显示未登录 | 中 | | ④ 账号管理 | Shopee 账号(账号名/别名/数据目录/端口/密码本地明文仅参考/登录状态);启动登录、检测登录、生成快捷方式;启动登录必须复用已打开的同账号 Chrome,避免重复开窗口;检测登录遇到 `accounts.shopee.tw/seller/login` 必须显示未登录 | 中 |
| ⑤ 设置 | cmhub 网关/API Key、生文/生图别名、生成参数、本地图片目录、Chrome 路径、默认端口、Shopee 更新安全开关等 | — | | ⑤ 设置 | cmhub 网关/API Key、生文/生图别名、生成参数、本地图片目录、Chrome 路径、默认端口、蝦皮更新安全开关等 | — |
任务的**阶段状态**贯穿各 Tab:`imported → collected → generated → applied`(或 `failed/skipped`)。② 不设逐条人工确认阶段;③ 无常驻提交开关,但点击「开始更新」后必须先通过 ⑤ 的 Shopee 更新安全开关,再弹窗确认当前筛选范围和任务数量。各 Tab 聚焦各自阶段的列与按钮,但操作同一批任务(同一 batch)。 任务的**阶段状态**贯穿各 Tab:`imported → collected → generated → applied`(或 `failed/skipped`)。② 不设逐条人工确认阶段;③ 无常驻提交开关,但点击「开始更新」后必须先通过 ⑤ 的蝦皮更新安全开关,再弹窗确认当前筛选范围和任务数量。各 Tab 聚焦各自阶段的列与按钮,但操作同一批任务(同一 batch)。
## 全局 Tab 栏可用性 ## 全局 Tab 栏可用性
@@ -29,7 +29,7 @@
## 首次使用引导保护 ## 首次使用引导保护
- ① 导入采集 与 ③ 更新shopee 都依赖**账号已配置且已登录**(在 ④ 账号管理)。 - ① 导入采集 与 ③ 更新蝦皮 都依赖**账号已配置且已登录**(在 ④ 账号管理)。
- 当无账号 / 对应账号 Chrome 未启动 / 账号未登录时:相关执行按钮**禁用或在执行前汇总拦截**,并提示「请先到『账号管理』配置账号并登录」。 - 当无账号 / 对应账号 Chrome 未启动 / 账号未登录时:相关执行按钮**禁用或在执行前汇总拦截**,并提示「请先到『账号管理』配置账号并登录」。
- ③ 点击「开始更新」后必须检查当前筛选结果涉及的账号;只要有账号 Chrome 未启动、CDP 端口不可访问或 Shopee 未登录,就弹窗列出账号并中止本轮更新,不创建真实更新 worker,不提交任何商品。 - ③ 点击「开始更新」后必须检查当前筛选结果涉及的账号;只要有账号 Chrome 未启动、CDP 端口不可访问或 Shopee 未登录,就弹窗列出账号并中止本轮更新,不创建真实更新 worker,不提交任何商品。
- 可以提供「打开账号管理」或「启动登录」入口辅助用户处理当前账号;不要无提示批量启动所有账号 Chrome,避免开错账号或启动过多浏览器进程。用户主动点击④「启动登录」时也必须先检查该账号 CDP 端口,已打开则复用现有 Chrome 并打开/激活卖家中心 tab,不重复 `Popen` 新窗口。 - 可以提供「打开账号管理」或「启动登录」入口辅助用户处理当前账号;不要无提示批量启动所有账号 Chrome,避免开错账号或启动过多浏览器进程。用户主动点击④「启动登录」时也必须先检查该账号 CDP 端口,已打开则复用现有 Chrome 并打开/激活卖家中心 tab,不重复 `Popen` 新窗口。
@@ -59,7 +59,7 @@
- 回写:采集完成后自动把旧标题/旧封面路径批量回写原 Excel;保留「回写旧数据到 Excel」作为手动重试入口(原文件被锁→提示关闭后重试/另存)。 - 回写:采集完成后自动把旧标题/旧封面路径批量回写原 Excel;保留「回写旧数据到 Excel」作为手动重试入口(原文件被锁→提示关闭后重试/另存)。
- 别名未匹配账号 / 账号未登录 → 该行 skipped 并记原因。`T-207` 接入后,① 采集会像③更新一样写 `run_logs/run_log_events`,并把完整脱敏 traceback 写入本地 `data/logs/`,用于定位失败卡在哪个步骤。 - 别名未匹配账号 / 账号未登录 → 该行 skipped 并记原因。`T-207` 接入后,① 采集会像③更新一样写 `run_logs/run_log_events`,并把完整脱敏 traceback 写入本地 `data/logs/`,用于定位失败卡在哪个步骤。
- 「删除批次」位于①批次筛选旁,只能对当前选中的具体批次执行,不能在“全部批次”下执行;运行中禁用。删除是软删除:写本地批次删除标记,不物理删除 DB 记录,不删除原 Excel,不回滚 Shopee。删除后该批次不再出现在①/②/③任何批次下拉、任务列表、筛选、采集、生成、更新、回写入口中。确认框必须显示任务数、已上线任务数,并提示软删除只隐藏本地批次、不会回滚线上修改。 - 「删除批次」位于①批次筛选旁,只能对当前选中的具体批次执行,不能在“全部批次”下执行;运行中禁用。删除是软删除:写本地批次删除标记,不物理删除 DB 记录,不删除原 Excel,不回滚蝦皮。删除后该批次不再出现在①/②/③任何批次下拉、任务列表、筛选、采集、生成、更新、回写入口中。确认框必须显示任务数、已上线任务数,并提示软删除只隐藏本地批次、不会回滚线上修改。
## ② AI生成 ## ② AI生成
左右布局:左侧约 1/4 放提示词,右侧放筛选 + 任务列表。 左右布局:左侧约 1/4 放提示词,右侧放筛选 + 任务列表。
@@ -89,13 +89,13 @@
- 底部**单个「开始生成」+「停止」**,并增加「生成封面图片(成本较高)」复选框:默认不勾选,只生成标题;勾选后才在标题完成后按 `image_concurrency` 并发生成封面。该开关状态持久化到 `config.json` 的 `ai.generate_cover`,但入口放在②,便于用户在每轮生成前做成本判断。 - 底部**单个「开始生成」+「停止」**,并增加「生成封面图片(成本较高)」复选框:默认不勾选,只生成标题;勾选后才在标题完成后按 `image_concurrency` 并发生成封面。该开关状态持久化到 `config.json` 的 `ai.generate_cover`,但入口放在②,便于用户在每轮生成前做成本判断。
- 生成参数(标题/图片并发数、失败重试、分辨率、jpg 质量、cmhub 网关/Key/别名)在 **⑤ 设置**;②只暴露“本轮是否生成封面”的成本开关。 - 生成参数(标题/图片并发数、失败重试、分辨率、jpg 质量、cmhub 网关/Key/别名)在 **⑤ 设置**;②只暴露“本轮是否生成封面”的成本开关。
- 标题-only 模式标题成功即写库并进入 `generated`,`new_cover_path` 留空;标题+封面模式图片成功后写入本地新封面路径。两种模式都写 `run_type=generate` 的 `run_logs/run_log_events` 和用户可读滚动日志;未勾选生成封面时日志明确显示“本轮仅生成标题”。点击「开始生成」时先清空②界面可见日志并写入本轮开始摘要,运行中只追加本轮日志;不删除历史 `run_logs/run_log_events` 或本地 `data/logs/`。进入页面默认可显示“本轮日志会在开始运行后显示”,历史日志不自动混入当前运行界面。「停止」取消未开始项,可再次「开始生成」对剩余继续。 - 标题-only 模式标题成功即写库并进入 `generated`,`new_cover_path` 留空;标题+封面模式图片成功后写入本地新封面路径。两种模式都写 `run_type=generate` 的 `run_logs/run_log_events` 和用户可读滚动日志;未勾选生成封面时日志明确显示“本轮仅生成标题”。点击「开始生成」时先清空②界面可见日志并写入本轮开始摘要,运行中只追加本轮日志;不删除历史 `run_logs/run_log_events` 或本地 `data/logs/`。进入页面默认可显示“本轮日志会在开始运行后显示”,历史日志不自动混入当前运行界面。「停止」取消未开始项,可再次「开始生成」对剩余继续。
- 「重置生成结果」仅作用当前选中单条,运行中禁用;确认后只改本地 DB,清空 `new_title/new_cover_path/last_error` 并退回 `collected/success` 供重新生成,默认不删除本地新封面文件。若该记录已提交过线上,必须在确认框提示本地重置不回滚 Shopee。 - 「重置生成结果」仅作用当前选中单条,运行中禁用;确认后只改本地 DB,清空 `new_title/new_cover_path/last_error` 并退回 `collected/success` 供重新生成,默认不删除本地新封面文件。若该记录已提交过线上,必须在确认框提示本地重置不回滚蝦皮。
- **无逐条人工审核环节**;新标题默认直接用 AI 输出,但允许对已生成且未提交线上的单行做本地微调;可选对单行 `重生成`。生成完即可进入 ③,③ 开始更新前会做批量确认。 - **无逐条人工审核环节**;新标题默认直接用 AI 输出,但允许对已生成且未提交线上的单行做本地微调;可选对单行 `重生成`。生成完即可进入 ③,③ 开始更新前会做批量确认。
## ③ 更新shopee ## ③ 更新蝦皮
``` ```
┌ 更新shopee ───────────────────────────────────────────────────┐ ┌ 更新蝦皮 ───────────────────────────────────────────────────┐
│ 批次[本次▼] 店铺[全部▼] 商品ID[____] 状态[已生成▼] [筛选] │ │ 批次[本次▼] 店铺[全部▼] 商品ID[____] 状态[已生成▼] [筛选] │
│ ⚠ 点击「开始更新」后先过安全开关,再确认【当前筛选结果】并提交 │ │ ⚠ 点击「开始更新」后先过安全开关,再确认【当前筛选结果】并提交 │
│ ┌───────────────────────────────────────────────────────────┐ │ │ ┌───────────────────────────────────────────────────────────┐ │
@@ -124,7 +124,7 @@
- 更新前做账号就绪预检:无账号、对应账号 Chrome 未启动、CDP 端口不可访问或未登录时整体阻断并引导去④账号管理,不进入逐条提交,也不自动打开账号 Chrome。 - 更新前做账号就绪预检:无账号、对应账号 Chrome 未启动、CDP 端口不可访问或未登录时整体阻断并引导去④账号管理,不进入逐条提交,也不自动打开账号 Chrome。
- 若 ⑤ 开启“成功后关闭本次新开编辑页”,则仅关闭本轮程序自动新开且成功提交的商品页;确认后跳回我的商品列表页时,关闭前等待 2 秒;已进入编辑页后的失败任务和用户原本打开的 tab 不关闭。但如果失败发生在 `open_product()` 内部,程序自动新开的无效商品页 tab 要关闭,避免 Chrome 残留空错误页。 - 若 ⑤ 开启“成功后关闭本次新开编辑页”,则仅关闭本轮程序自动新开且成功提交的商品页;确认后跳回我的商品列表页时,关闭前等待 2 秒;已进入编辑页后的失败任务和用户原本打开的 tab 不关闭。但如果失败发生在 `open_product()` 内部,程序自动新开的无效商品页 tab 要关闭,避免 Chrome 残留空错误页。
- 「开始更新」是③的主操作按钮,视觉上必须强于其他批处理按钮。 - 「开始更新」是③的主操作按钮,视觉上必须强于其他批处理按钮。
- 「重置更新状态」从底部批处理按钮移到任务表右键菜单/高级入口,仅作用当前选中单条,运行中禁用;保留 `new_title/new_cover_path`,只把本地状态退回可更新,用于重复测试上传/提交。若 `committed=1`,确认框必须提示线上已提交过、本地重置不回滚 Shopee、重复更新会再次提交;不得静默清除 committed 历史事实。 - 「重置更新状态」从底部批处理按钮移到任务表右键菜单/高级入口,仅作用当前选中单条,运行中禁用;保留 `new_title/new_cover_path`,只把本地状态退回可更新,用于重复测试上传/提交。若 `committed=1`,确认框必须提示线上已提交过、本地重置不回滚蝦皮、重复更新会再次提交;不得静默清除 committed 历史事实。
- 更新完成后自动回写原 Excel:写入新标题、新封面图片路径、更新状态;原文件被锁时提示关闭后点击「回写结果到 Excel」手动重试。 - 更新完成后自动回写原 Excel:写入新标题、新封面图片路径、更新状态;原文件被锁时提示关闭后点击「回写结果到 Excel」手动重试。
- 自动回写完成后弹窗汇总成功/失败/略过数量与 Excel 回写文件/行数。 - 自动回写完成后弹窗汇总成功/失败/略过数量与 Excel 回写文件/行数。
@@ -154,7 +154,7 @@
- 分辨率为 `512 / 1k / 2k / 4k`;返回超时只读展示 `resolution_timeouts[resolution]`,不单独编辑。 - 分辨率为 `512 / 1k / 2k / 4k`;返回超时只读展示 `resolution_timeouts[resolution]`,不单独编辑。
- 保存写入 `config.json` 的 `ai` 段,供 ② AI生成复用;标题/图片模型角色下拉随 direct UI 一起隐藏。 - 保存写入 `config.json` 的 `ai` 段,供 ② AI生成复用;标题/图片模型角色下拉随 direct UI 一起隐藏。
- 路径与端口(T-501b/T-506 已接入):组件组改为 3 个组件一组;默认端口、端口起止、CDP 就绪超时等短字段三列排列;Chrome 路径、账号数据根目录、图片目录、DB 路径等长字段跨整行或跨 2/3 列。 - 路径与端口(T-501b/T-506 已接入):组件组改为 3 个组件一组;默认端口、端口起止、CDP 就绪超时等短字段三列排列;Chrome 路径、账号数据根目录、图片目录、DB 路径等长字段跨整行或跨 2/3 列。
- Shopee 更新安全(T-501c/T-506 已接入):组件组改为 3 个组件一组;允许真实提交、允许更新封面、每批最大更新条数、成功后关闭本次新开编辑页等短字段三列排列;「多账号并行更新」与「最大并行账号数」必须合并为同一个横向组件,最大并行账号数紧跟在多账号并行更新后面,不允许换到下一行;测试商品 ID 仅作为历史/调试兼容字段保留,不参与普通正式更新安全检查,普通设置页已隐藏该入口。 - 蝦皮更新安全(T-501c/T-506 已接入):组件组改为 3 个组件一组;允许真实提交、允许更新封面、每批最大更新条数、成功后关闭本次新开编辑页等短字段三列排列;「多账号并行更新」与「最大并行账号数」必须合并为同一个横向组件,最大并行账号数紧跟在多账号并行更新后面,不允许换到下一行;测试商品 ID 仅作为历史/调试兼容字段保留,不参与普通正式更新安全检查,普通设置页已隐藏该入口。
- 默认关闭真实提交和封面更新,每批最大更新条数默认 1。 - 默认关闭真实提交和封面更新,每批最大更新条数默认 1。
- ③ 点击「开始更新」会读取这些设置,先拦截不符合条件的更新,再弹确认框。 - ③ 点击「开始更新」会读取这些设置,先拦截不符合条件的更新,再弹确认框。
- 更新执行模式(T-504/T-506 已接入):⑤ 只保留多账号并行更新、最大并行账号数等执行设置;“dry-run”不再作为用户可见开关,改到③成为「检查本轮更新」按钮。 - 更新执行模式(T-504/T-506 已接入):⑤ 只保留多账号并行更新、最大并行账号数等执行设置;“dry-run”不再作为用户可见开关,改到③成为「检查本轮更新」按钮。
@@ -169,9 +169,9 @@
│ │
② AI生成:提示词 → 生成新标题/可选生成新封面(无逐条审核) ② AI生成:提示词 → 生成新标题/可选生成新封面(无逐条审核)
│ │
⑤ 设置:配置 Shopee 更新安全、每批最大更新条数和执行模式 ⑤ 设置:配置蝦皮更新安全、每批最大更新条数和执行模式
│ │
③ 更新shopee:对已生成任务点击开始更新 → 安全开关检查 → 弹窗确认 → 账号就绪预检(未启动/未登录则中止) → 换标题/允许时换封面 → 点「更新」提交 → 回写结果 → 弹窗汇总 ③ 更新蝦皮:对已生成任务点击开始更新 → 安全开关检查 → 弹窗确认 → 账号就绪预检(未启动/未登录则中止) → 换标题/允许时换封面 → 点「更新」提交 → 回写结果 → 弹窗汇总
``` ```
- 未配账号 / Chrome 未启动 / 未登录:① ③ 的执行按钮禁用或执行前提示去 ④;③ 不静默启动缺失账号 Chrome。 - 未配账号 / Chrome 未启动 / 未登录:① ③ 的执行按钮禁用或执行前提示去 ④;③ 不静默启动缺失账号 Chrome。
@@ -185,9 +185,9 @@
| `MainWindow(QMainWindow)` | 根窗口 | 持有 `QTabWidget`、状态栏、全局消息 | | `MainWindow(QMainWindow)` | 根窗口 | 持有 `QTabWidget`、状态栏、全局消息 |
| `CollectTab(QWidget)` | ① | 导入、任务表、采集、回写 | | `CollectTab(QWidget)` | ① | 导入、任务表、采集、回写 |
| `GenerateTab(QWidget)` | ② | 左提示词管理 + 右筛选/任务列表;双击看新旧封面;开始生成/停止/进度;本轮「生成封面图片(成本较高)」开关接入 `GenerateWorker` | | `GenerateTab(QWidget)` | ② | 左提示词管理 + 右筛选/任务列表;双击看新旧封面;开始生成/停止/进度;本轮「生成封面图片(成本较高)」开关接入 `GenerateWorker` |
| `ApplyTab(QWidget)` | ③ | 已生成任务筛选 +「检查本轮更新」+ Shopee 更新安全拦截 + 分批开始更新确认 + 检查/真实更新运行日志 + 结果回写与结束汇总 | | `ApplyTab(QWidget)` | ③ | 已生成任务筛选 +「检查本轮更新」+ 蝦皮更新安全拦截 + 分批开始更新确认 + 检查/真实更新运行日志 + 结果回写与结束汇总 |
| `AccountsTab(QWidget)` | ④ | 账号增删改、启动登录、检测登录、生成快捷方式;登录检测把 Shopee accounts 登录页判为未登录 | | `AccountsTab(QWidget)` | ④ | 账号增删改、启动登录、检测登录、生成快捷方式;登录检测把 Shopee accounts 登录页判为未登录 |
| `SettingsTab(QWidget)` | ⑤ | AI 模型 master-detail 管理 + 响应式三列设置表单 + 角色/生成参数/路径/端口配置 + Shopee 更新安全 | | `SettingsTab(QWidget)` | ⑤ | AI 模型 master-detail 管理 + 响应式三列设置表单 + 角色/生成参数/路径/端口配置 + 蝦皮更新安全 |
| `TaskTableModel(QAbstractTableModel)` | ①②③ | 任务表格数据模型,供 `QTableView` 使用 | | `TaskTableModel(QAbstractTableModel)` | ①②③ | 任务表格数据模型,供 `QTableView` 使用 |
| `BaseWorker(QObject)` | 后台 | 定义 `progress/log/row_updated/failed/finished/cancelled` signals | | `BaseWorker(QObject)` | 后台 | 定义 `progress/log/row_updated/failed/finished/cancelled` signals |
| `ApplyWorker(BaseWorker)` | ③ | 账号就绪预检、检查本轮更新、按每批最大条数分批、按账号并行或串行调用 `editor.apply_task(..., close_success_tab=...)`、逐条 `set_applied()`,失败继续,写运行日志 | | `ApplyWorker(BaseWorker)` | ③ | 账号就绪预检、检查本轮更新、按每批最大条数分批、按账号并行或串行调用 `editor.apply_task(..., close_success_tab=...)`、逐条 `set_applied()`,失败继续,写运行日志 |
+1 -1
View File
@@ -7,7 +7,7 @@
| [overview-pipeline.svg](overview-pipeline.svg) | 5 Tab 流水线总览(stage:imported→collected→generated→applied) | | [overview-pipeline.svg](overview-pipeline.svg) | 5 Tab 流水线总览(stage:imported→collected→generated→applied) |
| [tab1-import-collect.svg](tab1-import-collect.svg) | ① 导入采集:导入 Excel、任务列表、采集旧标题/旧封面、回写、日志 | | [tab1-import-collect.svg](tab1-import-collect.svg) | ① 导入采集:导入 Excel、任务列表、采集旧标题/旧封面、回写、日志 |
| [tab2-ai-generate.svg](tab2-ai-generate.svg) | ② AI生成:提示词、生成、新旧对照预览、重生成 | | [tab2-ai-generate.svg](tab2-ai-generate.svg) | ② AI生成:提示词、生成、新旧对照预览、重生成 |
| [tab3-update-shopee.svg](tab3-update-shopee.svg) | ③ 更新shopee:已生成任务、开始更新确认、执行结果、汇总 | | [tab3-update-shopee.svg](tab3-update-shopee.svg) | ③ 更新蝦皮:已生成任务、开始更新确认、执行结果、汇总 |
| [tab4-accounts.svg](tab4-accounts.svg) | ④ 账号管理:账号列表、启动登录、检测登录、账号编辑弹窗 | | [tab4-accounts.svg](tab4-accounts.svg) | ④ 账号管理:账号列表、启动登录、检测登录、账号编辑弹窗 |
| [tab5-settings.svg](tab5-settings.svg) | ⑤ 设置:AI 模型/Key、路径、端口 | | [tab5-settings.svg](tab5-settings.svg) | ⑤ 设置:AI 模型/Key、路径、端口 |
+1 -1
View File
@@ -53,7 +53,7 @@
</g> </g>
<g transform="translate(386,0)"> <g transform="translate(386,0)">
<rect class="card" x="0" y="0" width="168" height="210" rx="8"/> <rect class="card" x="0" y="0" width="168" height="210" rx="8"/>
<text class="tt" x="14" y="28">③ 更新shopee</text> <text class="tt" x="14" y="28">③ 更新蝦皮</text>
<text class="st" x="14" y="52">已生成任务</text> <text class="st" x="14" y="52">已生成任务</text>
<text class="ds" x="14" y="72">打开编辑页</text> <text class="ds" x="14" y="72">打开编辑页</text>
<text class="ds" x="14" y="90">换标题 + 换封面</text> <text class="ds" x="14" y="90">换标题 + 换封面</text>

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

+2 -2
View File
@@ -25,7 +25,7 @@
<rect class="win" x="0.5" y="0.5" width="939" height="619" rx="8"/> <rect class="win" x="0.5" y="0.5" width="939" height="619" rx="8"/>
<rect class="hd" x="0.5" y="0.5" width="939" height="40" rx="8"/> <rect class="hd" x="0.5" y="0.5" width="939" height="40" rx="8"/>
<rect class="hd" x="0.5" y="20" width="939" height="21"/> <rect class="hd" x="0.5" y="20" width="939" height="21"/>
<text class="title" x="18" y="26">蝦皮圈優化助手 · Shopee 商品批量改标题换封面</text> <text class="title" x="18" y="26">蝦皮圈優化助手 · 蝦皮商品批量改标题换封面</text>
<circle cx="888" cy="20" r="6" fill="#f5bf4f"/><circle cx="910" cy="20" r="6" fill="#5ac26a"/><circle cx="866" cy="20" r="6" fill="#e0e4ea"/> <circle cx="888" cy="20" r="6" fill="#f5bf4f"/><circle cx="910" cy="20" r="6" fill="#5ac26a"/><circle cx="866" cy="20" r="6" fill="#e0e4ea"/>
<!-- tab strip --> <!-- tab strip -->
@@ -35,7 +35,7 @@
<rect x="8" y="45" width="150" height="3" fill="#2f6fed"/> <rect x="8" y="45" width="150" height="3" fill="#2f6fed"/>
<text class="tabTA" x="34" y="68">① 导入采集</text> <text class="tabTA" x="34" y="68">① 导入采集</text>
<text class="tabT" x="186" y="68">② AI生成</text> <text class="tabT" x="186" y="68">② AI生成</text>
<text class="tabT" x="330" y="68">③ 更新shopee</text> <text class="tabT" x="330" y="68">③ 更新蝦皮</text>
<text class="tabT" x="492" y="68">④ 账号管理</text> <text class="tabT" x="492" y="68">④ 账号管理</text>
<text class="tabT" x="636" y="68">⑤ 设置</text> <text class="tabT" x="636" y="68">⑤ 设置</text>
</g> </g>

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

+2 -2
View File
@@ -12,14 +12,14 @@
</defs> </defs>
<rect class="win" x="0.5" y="0.5" width="939" height="619" rx="8"/> <rect class="win" x="0.5" y="0.5" width="939" height="619" rx="8"/>
<rect class="hd" x="0.5" y="0.5" width="939" height="40" rx="8"/><rect class="hd" x="0.5" y="20" width="939" height="21"/> <rect class="hd" x="0.5" y="0.5" width="939" height="40" rx="8"/><rect class="hd" x="0.5" y="20" width="939" height="21"/>
<text class="title" x="18" y="26">蝦皮圈優化助手 · Shopee 商品批量改标题换封面</text> <text class="title" x="18" y="26">蝦皮圈優化助手 · 蝦皮商品批量改标题换封面</text>
<circle cx="888" cy="20" r="6" fill="#f5bf4f"/><circle cx="910" cy="20" r="6" fill="#5ac26a"/><circle cx="866" cy="20" r="6" fill="#e0e4ea"/> <circle cx="888" cy="20" r="6" fill="#f5bf4f"/><circle cx="910" cy="20" r="6" fill="#5ac26a"/><circle cx="866" cy="20" r="6" fill="#e0e4ea"/>
<rect class="tab" x="0.5" y="41" width="939" height="38"/> <rect class="tab" x="0.5" y="41" width="939" height="38"/>
<rect class="tabA" x="166" y="45" width="138" height="38"/><rect x="166" y="45" width="138" height="3" fill="#2f6fed"/> <rect class="tabA" x="166" y="45" width="138" height="38"/><rect x="166" y="45" width="138" height="3" fill="#2f6fed"/>
<text class="tabT" x="34" y="68">① 导入采集</text> <text class="tabT" x="34" y="68">① 导入采集</text>
<text class="tabTA" x="196" y="68">② AI生成</text> <text class="tabTA" x="196" y="68">② AI生成</text>
<text class="tabT" x="330" y="68">③ 更新shopee</text> <text class="tabT" x="330" y="68">③ 更新蝦皮</text>
<text class="tabT" x="492" y="68">④ 账号管理</text> <text class="tabT" x="492" y="68">④ 账号管理</text>
<text class="tabT" x="636" y="68">⑤ 设置</text> <text class="tabT" x="636" y="68">⑤ 设置</text>

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

+2 -2
View File
@@ -9,13 +9,13 @@
</style></defs> </style></defs>
<rect class="win" x="0.5" y="0.5" width="939" height="619" rx="8"/> <rect class="win" x="0.5" y="0.5" width="939" height="619" rx="8"/>
<rect class="hd" x="0.5" y="0.5" width="939" height="40" rx="8"/><rect class="hd" x="0.5" y="20" width="939" height="21"/> <rect class="hd" x="0.5" y="0.5" width="939" height="40" rx="8"/><rect class="hd" x="0.5" y="20" width="939" height="21"/>
<text class="title" x="18" y="26">蝦皮圈優化助手 · Shopee 商品批量改标题换封面</text> <text class="title" x="18" y="26">蝦皮圈優化助手 · 蝦皮商品批量改标题换封面</text>
<circle cx="888" cy="20" r="6" fill="#f5bf4f"/><circle cx="910" cy="20" r="6" fill="#5ac26a"/><circle cx="866" cy="20" r="6" fill="#e0e4ea"/> <circle cx="888" cy="20" r="6" fill="#f5bf4f"/><circle cx="910" cy="20" r="6" fill="#5ac26a"/><circle cx="866" cy="20" r="6" fill="#e0e4ea"/>
<rect class="tab" x="0.5" y="41" width="939" height="38"/> <rect class="tab" x="0.5" y="41" width="939" height="38"/>
<rect class="tabA" x="304" y="45" width="162" height="38"/><rect x="304" y="45" width="162" height="3" fill="#2f6fed"/> <rect class="tabA" x="304" y="45" width="162" height="38"/><rect x="304" y="45" width="162" height="3" fill="#2f6fed"/>
<text class="tabT" x="34" y="68">① 导入采集</text><text class="tabT" x="186" y="68">② AI生成</text> <text class="tabT" x="34" y="68">① 导入采集</text><text class="tabT" x="186" y="68">② AI生成</text>
<text class="tabTA" x="334" y="68">③ 更新shopee</text> <text class="tabTA" x="334" y="68">③ 更新蝦皮</text>
<text class="tabT" x="492" y="68">④ 账号管理</text><text class="tabT" x="636" y="68">⑤ 设置</text> <text class="tabT" x="492" y="68">④ 账号管理</text><text class="tabT" x="636" y="68">⑤ 设置</text>
<!-- filter bar --> <!-- filter bar -->

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

+2 -2
View File
@@ -9,13 +9,13 @@
</style></defs> </style></defs>
<rect class="win" x="0.5" y="0.5" width="939" height="619" rx="8"/> <rect class="win" x="0.5" y="0.5" width="939" height="619" rx="8"/>
<rect class="hd" x="0.5" y="0.5" width="939" height="40" rx="8"/><rect class="hd" x="0.5" y="20" width="939" height="21"/> <rect class="hd" x="0.5" y="0.5" width="939" height="40" rx="8"/><rect class="hd" x="0.5" y="20" width="939" height="21"/>
<text class="title" x="18" y="26">蝦皮圈優化助手 · Shopee 商品批量改标题换封面</text> <text class="title" x="18" y="26">蝦皮圈優化助手 · 蝦皮商品批量改标题换封面</text>
<circle cx="888" cy="20" r="6" fill="#f5bf4f"/><circle cx="910" cy="20" r="6" fill="#5ac26a"/><circle cx="866" cy="20" r="6" fill="#e0e4ea"/> <circle cx="888" cy="20" r="6" fill="#f5bf4f"/><circle cx="910" cy="20" r="6" fill="#5ac26a"/><circle cx="866" cy="20" r="6" fill="#e0e4ea"/>
<rect class="tab" x="0.5" y="41" width="939" height="38"/> <rect class="tab" x="0.5" y="41" width="939" height="38"/>
<rect class="tabA" x="466" y="45" width="150" height="38"/><rect x="466" y="45" width="150" height="3" fill="#2f6fed"/> <rect class="tabA" x="466" y="45" width="150" height="38"/><rect x="466" y="45" width="150" height="3" fill="#2f6fed"/>
<text class="tabT" x="34" y="68">① 导入采集</text><text class="tabT" x="186" y="68">② AI生成</text> <text class="tabT" x="34" y="68">① 导入采集</text><text class="tabT" x="186" y="68">② AI生成</text>
<text class="tabT" x="330" y="68">③ 更新shopee</text> <text class="tabT" x="330" y="68">③ 更新蝦皮</text>
<text class="tabTA" x="496" y="68">④ 账号管理</text> <text class="tabTA" x="496" y="68">④ 账号管理</text>
<text class="tabT" x="636" y="68">⑤ 设置</text> <text class="tabT" x="636" y="68">⑤ 设置</text>

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

+2 -2
View File
@@ -9,13 +9,13 @@
</style></defs> </style></defs>
<rect class="win" x="0.5" y="0.5" width="939" height="619" rx="8"/> <rect class="win" x="0.5" y="0.5" width="939" height="619" rx="8"/>
<rect class="hd" x="0.5" y="0.5" width="939" height="40" rx="8"/><rect class="hd" x="0.5" y="20" width="939" height="21"/> <rect class="hd" x="0.5" y="0.5" width="939" height="40" rx="8"/><rect class="hd" x="0.5" y="20" width="939" height="21"/>
<text class="title" x="18" y="26">蝦皮圈優化助手 · Shopee 商品批量改标题换封面</text> <text class="title" x="18" y="26">蝦皮圈優化助手 · 蝦皮商品批量改标题换封面</text>
<circle cx="888" cy="20" r="6" fill="#f5bf4f"/><circle cx="910" cy="20" r="6" fill="#5ac26a"/><circle cx="866" cy="20" r="6" fill="#e0e4ea"/> <circle cx="888" cy="20" r="6" fill="#f5bf4f"/><circle cx="910" cy="20" r="6" fill="#5ac26a"/><circle cx="866" cy="20" r="6" fill="#e0e4ea"/>
<rect class="tab" x="0.5" y="41" width="939" height="38"/> <rect class="tab" x="0.5" y="41" width="939" height="38"/>
<rect class="tabA" x="616" y="45" width="110" height="38"/><rect x="616" y="45" width="110" height="3" fill="#2f6fed"/> <rect class="tabA" x="616" y="45" width="110" height="38"/><rect x="616" y="45" width="110" height="3" fill="#2f6fed"/>
<text class="tabT" x="34" y="68">① 导入采集</text><text class="tabT" x="186" y="68">② AI生成</text> <text class="tabT" x="34" y="68">① 导入采集</text><text class="tabT" x="186" y="68">② AI生成</text>
<text class="tabT" x="330" y="68">③ 更新shopee</text><text class="tabT" x="492" y="68">④ 账号管理</text> <text class="tabT" x="330" y="68">③ 更新蝦皮</text><text class="tabT" x="492" y="68">④ 账号管理</text>
<text class="tabTA" x="640" y="68">⑤ 设置</text> <text class="tabTA" x="640" y="68">⑤ 设置</text>
<!-- A. AI 模型管理 (single-column master-detail) --> <!-- A. AI 模型管理 (single-column master-detail) -->

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

+19
View File
@@ -1351,3 +1351,22 @@
- 现象:T-540 代码和 `requirements-build.txt` 已固定 PyInstaller 6.11.1,构建脚本也会校验 `_internal/`,但 `docs/packaging.md`、`docs/03-tech-stack.md`、`docs/06-tasks.md`、`docs/current-state.md` 仍有当前 PyInstaller 5.13.2 扁平 onedir 的表述。 - 现象:T-540 代码和 `requirements-build.txt` 已固定 PyInstaller 6.11.1,构建脚本也会校验 `_internal/`,但 `docs/packaging.md`、`docs/03-tech-stack.md`、`docs/06-tasks.md`、`docs/current-state.md` 仍有当前 PyInstaller 5.13.2 扁平 onedir 的表述。
- 文档:已把当前打包口径统一为 PyInstaller 6.11.1,产物必须包含 `cmshopee.exe` + `_internal/`;用户升级说明改为整体覆盖新版程序文件、`_internal/`、`version.txt`、`README.txt`,并保留旧目录 `data/`。 - 文档:已把当前打包口径统一为 PyInstaller 6.11.1,产物必须包含 `cmshopee.exe` + `_internal/`;用户升级说明改为整体覆盖新版程序文件、`_internal/`、`version.txt`、`README.txt`,并保留旧目录 `data/`。
- 边界:只修正文档一致性;历史 T-538 流水仍保留当时 PyInstaller 5 的记录;未改业务流程、DB、AI、Excel、CDP/Shopee 或用户数据路径。 - 边界:只修正文档一致性;历史 T-538 流水仍保留当时 PyInstaller 5 的记录;未改业务流程、DB、AI、Excel、CDP/Shopee 或用户数据路径。
## 【2026-07-07】T-542 文档 · GUI 用户可见 shopee/Shopee 改为蝦皮
- 需求:运行程序后 GUI 上不再显示 `shopee/Shopee` 英文平台名,用户可见文案统一使用「蝦皮」,例如「③ 更新蝦皮」「蝦皮更新安全」。
- 文档:新增 `docs/06-tasks.md` T-542;同步 `docs/00-ai-start-here.md`、`docs/02-requirements.md`、`docs/04-architecture.md`、`docs/05-coding-rules.md`、`docs/routes.md`、`docs/api.md`、`docs/current-state.md` 与 `docs/ui/` 线框图。
- 边界:本轮只更新文档,不改代码;内部标识符、配置键、URL、域名、CDP 选择器、文件名和第三方原始错误仍保留 `shopee/Shopee`。
## 【2026-07-07】T-542 完成 · GUI 用户可见 shopee/Shopee 改为蝦皮
- 代码:`app/gui/widgets.py` 将 Tab 标题改为「③ 更新蝦皮」;`app/gui/main_window.py` 同步 Tab 判断和 warning 图标索引;③更新、⑤设置、①采集、②生成和 GUI worker 的用户可见提示统一改为「蝦皮」;`app/editor.py` 中会冒到 GUI 的自定义错误改为「蝦皮 CDN / 蝦皮商品编辑器」。
- 测试:`tests/test_gui.py` 同步新 Tab、设置分区和重置确认文案断言;旧用户文案验收搜索 `rg -n "更新shopee|Shopee 更新安全|更新 Shopee|未打开 Shopee|不回滚 Shopee" app tests` 已无命中。
- 验证:`python -m compileall app main.py` 通过;`python -m unittest discover -s tests` 通过(221 tests)。
- 边界:未改 `shopee_update`、`cmshopee`、域名、URL、CDP 选择器、JS 全局变量、DB/配置文件名等内部标识。
## 【2026-07-07】测试修正 · 避免根目录生成 db.sqlite
- 原因:`tests/test_gui.py` 中两个 `WriteBackWorker` 单测把 `db_path` 写成相对路径 `db.sqlite`;测试创建 run_log 时会打开 SQLite,导致在项目根目录生成一个空 `db.sqlite`。
- 修复:这两个测试改为使用 `self.make_temp_dir()` 下的 `db.sqlite`,并断言临时目录被清理;根目录空 `db.sqlite` 已删除。
- 验证:`python -m unittest discover -s tests -p test_gui.py` 通过(97 tests);`python -m unittest discover -s tests` 通过(221 tests),跑完后根目录没有 `db.sqlite`。
+19 -12
View File
@@ -140,7 +140,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
item_id="26887160467", item_id="26887160467",
stage="imported", stage="imported",
status="failed", status="failed",
last_error="等待 Shopee 商品编辑器就绪超时", last_error="等待蝦皮商品编辑器就绪超时",
) )
model = gui.TaskTableModel() model = gui.TaskTableModel()
model.set_tasks([invalid_task, generic_task], [account]) model.set_tasks([invalid_task, generic_task], [account])
@@ -166,7 +166,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual("就绪", window.statusBar().currentMessage()) self.assertEqual("就绪", window.statusBar().currentMessage())
self.assertEqual(gui.BUTTON_BASE_STYLE, window.styleSheet()) self.assertEqual(gui.BUTTON_BASE_STYLE, window.styleSheet())
self.assertEqual(TAB_STYLE, window.tabs.styleSheet()) self.assertEqual(TAB_STYLE, window.tabs.styleSheet())
self.assertFalse(window.tabs.tabIcon(TAB_TITLES.index("③ 更新shopee")).isNull()) self.assertFalse(window.tabs.tabIcon(TAB_TITLES.index("③ 更新蝦皮")).isNull())
self.assertIn("QPushButton", window.styleSheet()) self.assertIn("QPushButton", window.styleSheet())
self.assertIn("border-radius: 4px", window.styleSheet()) self.assertIn("border-radius: 4px", window.styleSheet())
self.assertIn("min-width: 128px", window.tabs.styleSheet()) self.assertIn("min-width: 128px", window.tabs.styleSheet())
@@ -404,7 +404,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertFalse(hasattr(tab, "test_item_id_edit")) self.assertFalse(hasattr(tab, "test_item_id_edit"))
self.assertFalse(hasattr(tab, "dry_run_checkbox")) self.assertFalse(hasattr(tab, "dry_run_checkbox"))
self.assertEqual( self.assertEqual(
"Shopee 更新安全 / 执行模式", "蝦皮更新安全 / 执行模式",
tab.shopee_update_section_title.text(), tab.shopee_update_section_title.text(),
) )
self.assertEqual("基础设施(路径与端口)", tab.infrastructure_section_title.text()) self.assertEqual("基础设施(路径与端口)", tab.infrastructure_section_title.text())
@@ -707,7 +707,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
) )
self.addCleanup(window.close) self.addCleanup(window.close)
settings_tab = window.tabs.widget(TAB_TITLES.index("⑤ 设置")) settings_tab = window.tabs.widget(TAB_TITLES.index("⑤ 设置"))
apply_tab = window.tabs.widget(TAB_TITLES.index("③ 更新shopee")) apply_tab = window.tabs.widget(TAB_TITLES.index("③ 更新蝦皮"))
settings_tab.allow_real_submit_checkbox.setChecked(True) settings_tab.allow_real_submit_checkbox.setChecked(True)
settings_tab.allow_cover_update_checkbox.setChecked(True) settings_tab.allow_cover_update_checkbox.setChecked(True)
@@ -2582,7 +2582,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
message = question.call_args[0][2] message = question.call_args[0][2]
self.assertIn("已经提交过线上", message) self.assertIn("已经提交过线上", message)
self.assertIn("本地重置不会回滚 Shopee", message) self.assertIn("本地重置不会回滚蝦皮", message)
self.assertIn("重复更新会再次提交线上", message) self.assertIn("重复更新会再次提交线上", message)
updated = db.get_task(task.id, path=cfg["db_path"]) updated = db.get_task(task.id, path=cfg["db_path"])
self.assertEqual("generated", updated.stage) self.assertEqual("generated", updated.stage)
@@ -3500,8 +3500,8 @@ class GuiTests(TempDirMixin, unittest.TestCase):
window.tabs.setCurrentIndex(2) window.tabs.setCurrentIndex(2)
self.assertEqual("③ 更新shopee", window.tabs.tabText(window.tabs.currentIndex())) self.assertEqual("③ 更新蝦皮", window.tabs.tabText(window.tabs.currentIndex()))
self.assertEqual("当前:③ 更新shopee", window.statusBar().currentMessage()) self.assertEqual("当前:③ 更新蝦皮", window.statusBar().currentMessage())
self.assert_removed(temp_dir) self.assert_removed(temp_dir)
@@ -3922,7 +3922,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertTrue(all(task.batch_id != batch_a for task in generate_tab.model.tasks)) self.assertTrue(all(task.batch_id != batch_a for task in generate_tab.model.tasks))
self.assertTrue(all(task.batch_id != batch_a for task in apply_tab.model.tasks)) self.assertTrue(all(task.batch_id != batch_a for task in apply_tab.model.tasks))
self.assertEqual([True], refresh_calls) self.assertEqual([True], refresh_calls)
self.assertIn("不会回滚 Shopee", question.call_args[0][2]) self.assertIn("不会回滚蝦皮", question.call_args[0][2])
self.assertIn("已软删除批次", info.call_args[0][2]) self.assertIn("已软删除批次", info.call_args[0][2])
self.assertIn("已软删除批次", statuses[-1]) self.assertIn("已软删除批次", statuses[-1])
@@ -4438,33 +4438,40 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir) 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 self.make_temp_dir() as temp_dir:
db_path = os.path.join(temp_dir, "db.sqlite")
with mock.patch( with mock.patch(
"app.gui.excel.write_back", "app.gui.excel.write_back",
return_value={"ok": True, "batch_id": "batch-1", "files": 1, "rows": 2}, return_value={"ok": True, "batch_id": "batch-1", "files": 1, "rows": 2},
) as write_back: ) as write_back:
summary = WriteBackWorker("batch-1", db_path="db.sqlite").execute() summary = WriteBackWorker("batch-1", db_path=db_path).execute()
self.assertEqual({"ok": True, "batch_id": "batch-1", "files": 1, "rows": 2}, summary) self.assertEqual({"ok": True, "batch_id": "batch-1", "files": 1, "rows": 2}, summary)
write_back.assert_called_once_with( write_back.assert_called_once_with(
"batch-1", "batch-1",
excel_path=None, excel_path=None,
path="db.sqlite", path=db_path,
) )
self.assert_removed(temp_dir)
def test_write_back_worker_calls_result_write_back(self): def test_write_back_worker_calls_result_write_back(self):
with self.make_temp_dir() as temp_dir:
db_path = os.path.join(temp_dir, "db.sqlite")
with mock.patch( with mock.patch(
"app.gui.excel.write_back_results", "app.gui.excel.write_back_results",
return_value={"ok": True, "batch_id": "batch-1", "files": 1, "rows": 2}, return_value={"ok": True, "batch_id": "batch-1", "files": 1, "rows": 2},
) as write_back_results: ) as write_back_results:
summary = WriteBackWorker("batch-1", db_path="db.sqlite", mode="results").execute() summary = WriteBackWorker("batch-1", db_path=db_path, mode="results").execute()
self.assertEqual({"ok": True, "batch_id": "batch-1", "files": 1, "rows": 2}, summary) self.assertEqual({"ok": True, "batch_id": "batch-1", "files": 1, "rows": 2}, summary)
write_back_results.assert_called_once_with( write_back_results.assert_called_once_with(
"batch-1", "batch-1",
excel_path=None, excel_path=None,
path="db.sqlite", path=db_path,
) )
self.assert_removed(temp_dir)
def test_collect_tab_import_excel_writes_diagnostic_run_log(self): def test_collect_tab_import_excel_writes_diagnostic_run_log(self):
with self.make_temp_dir() as temp_dir: with self.make_temp_dir() as temp_dir: