diff --git a/app/db.py b/app/db.py index 2c8301b..7c7b6ca 100644 --- a/app/db.py +++ b/app/db.py @@ -678,6 +678,35 @@ def set_generated(task_id, new_title, new_cover_path, path=None, conn=None) -> N (new_title, new_cover_path, now, now, int(task_id)), ) +def update_generated_title(task_id, new_title, path=None, conn=None) -> None: + """Update a generated title manually without touching Shopee or local cover files.""" + + title = str(new_title or "").strip() + if not title: + raise DbError("新标题不能为空") + with _connection(conn, path) as database: + task = get_task(task_id, conn=database) + if task is None: + raise DbError(f"任务不存在或批次已删除: {task_id}") + if int(task.committed or 0) == 1 or task.stage == "applied": + raise DbError("已提交线上商品不能在本地直接修改新标题") + if task.stage != "generated": + raise DbError("只有已生成且未提交线上的任务可以修改新标题") + if task.status == "running": + raise DbError("任务正在运行,不能修改新标题") + now = _now() + with database: + database.execute( + """ + UPDATE tasks + SET new_title = ?, + status = 'pending', + last_error = NULL, + updated_at = ? + WHERE id = ? + """, + (title, now, int(task_id)), + ) def set_applied(task_id, committed, error=None, path=None, conn=None) -> None: now = _now() diff --git a/app/gui.py b/app/gui.py index 9e44acb..db5548c 100644 --- a/app/gui.py +++ b/app/gui.py @@ -27,6 +27,7 @@ try: QLabel, QLineEdit, QMainWindow, + QMenu, QMessageBox, QPlainTextEdit, QPushButton, @@ -270,10 +271,13 @@ if QT_IMPORT_ERROR is None: "applied": "已更新", } - def __init__(self, parent=None): + def __init__(self, parent=None, db_path=None, status_callback=None): super().__init__(parent) self.tasks = [] self.account_by_alias = {} + self.db_path = db_path + self.status_callback = status_callback + self.last_edit_error = None def set_tasks(self, tasks, accounts): self.beginResetModel() @@ -302,16 +306,57 @@ if QT_IMPORT_ERROR is None: if not index.isValid(): return None task = self.tasks[index.row()] - if role == Qt.DisplayRole: + if role in (Qt.DisplayRole, Qt.EditRole): return self._display_value(task, index.column()) - if role == Qt.ToolTipRole and task.last_error: - return task.last_error + if role == Qt.ToolTipRole: + if index.column() == 3 and self._can_edit_title(task): + return "双击可微调新标题,只修改本地待更新内容" + if task.last_error: + return task.last_error return None + def setData(self, index, value, role=Qt.EditRole): + if role != Qt.EditRole or not index.isValid() or index.column() != 3: + return False + task = self.tasks[index.row()] + if not self._can_edit_title(task): + self._set_status("该任务不能修改新标题") + return False + title = str(value or "").strip() + if title == str(task.new_title or ""): + return True + try: + db.update_generated_title(task.id, title, path=self.db_path) + updated = db.get_task(task.id, path=self.db_path) + except Exception as exc: + self.last_edit_error = str(exc) + self._set_status(f"新标题修改失败:{exc}") + return False + self.tasks[index.row()] = updated + self.last_edit_error = None + self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole, Qt.ToolTipRole]) + self._set_status(f"已修改新标题:商品 {task.item_id}") + return True + def flags(self, index): if not index.isValid(): return Qt.NoItemFlags - return Qt.ItemIsEnabled | Qt.ItemIsSelectable + flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable + if index.column() == 3 and self._can_edit_title(self.tasks[index.row()]): + flags |= Qt.ItemIsEditable + return flags + + def _can_edit_title(self, task): + return ( + getattr(task, "stage", None) == "generated" + and getattr(task, "status", None) != "running" + and int(getattr(task, "committed", 0) or 0) == 0 + and bool(getattr(task, "new_title", None)) + ) + + def _set_status(self, message): + if self.status_callback is not None: + self.status_callback(message) def task_at(self, row): if row < 0 or row >= len(self.tasks): @@ -434,7 +479,6 @@ if QT_IMPORT_ERROR is None: return "待更新" return self.STATUS_TEXT.get(task.status, task.status) - class GenerateTab(QWidget): """Tab 2: prompt area plus generation task filters/list.""" @@ -539,11 +583,11 @@ if QT_IMPORT_ERROR is None: self.summary_label = QLabel("任务 0 条") self.task_table = QTableView() - self.model = GenerateTaskTableModel(self.task_table) + self.model = GenerateTaskTableModel(self.task_table, db_path=self.db_path, status_callback=self._set_status) self.task_table.setModel(self.model) self.task_table.setSelectionBehavior(QAbstractItemView.SelectRows) self.task_table.setSelectionMode(QAbstractItemView.SingleSelection) - self.task_table.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.task_table.setEditTriggers(QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed) self.task_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.task_table.verticalHeader().setVisible(False) @@ -870,6 +914,8 @@ if QT_IMPORT_ERROR is None: ) def show_task_images(self, index): + if index.isValid() and index.column() == 3: + return task = self.model.task_at(index.row()) if index.isValid() else self._selected_task() if task is None: self._set_status("没有可预览的任务") @@ -1120,12 +1166,14 @@ if QT_IMPORT_ERROR is None: config=None, status_callback=None, open_accounts_callback=None, + open_settings_callback=None, ): super().__init__(parent) self.config = appconfig.load_config() if config is None else config self.db_path = _database_path(db_path, self.config) self.status_callback = status_callback self.open_accounts_callback = open_accounts_callback + self.open_settings_callback = open_settings_callback self.apply_worker = None self.apply_thread = None self.result_write_back_worker = None @@ -1157,13 +1205,14 @@ if QT_IMPORT_ERROR is None: filter_layout.addWidget(self.refresh_button) self.summary_label = QLabel("任务 0 条") - self.risk_label = QLabel("可先点击「预览本轮更新」检查当前筛选范围;点击「开始更新」后会再次确认并按批提交线上。") + self.risk_label = QLabel("可先点击「检查本轮更新」确认当前筛选范围;点击「开始更新」后会再次确认并按批提交线上。") self.task_table = QTableView() self.model = ApplyTaskTableModel(self.task_table) self.task_table.setModel(self.model) self.task_table.setSelectionBehavior(QAbstractItemView.SelectRows) self.task_table.setSelectionMode(QAbstractItemView.SingleSelection) self.task_table.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.task_table.setContextMenuPolicy(Qt.CustomContextMenu) self.task_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.task_table.verticalHeader().setVisible(False) self.run_log_view = QPlainTextEdit() @@ -1172,12 +1221,18 @@ if QT_IMPORT_ERROR is None: self.run_log_view.setMaximumHeight(128) self.run_log_view.setPlaceholderText("运行日志") - self.preview_update_button = QPushButton("预览本轮更新") + self.preview_update_button = QPushButton("检查本轮更新") self.preview_update_button.setObjectName("previewUpdateButton") self.start_update_button = QPushButton("开始更新") + self.start_update_button.setObjectName("startUpdateButton") + self.start_update_button.setMinimumWidth(118) + self.start_update_button.setStyleSheet( + "QPushButton#startUpdateButton { font-weight: 600; padding: 6px 16px; }" + ) self.stop_update_button = QPushButton("停止") self.reset_update_button = QPushButton("重置更新状态") self.reset_update_button.setObjectName("resetUpdateButton") + self.reset_update_button.setVisible(False) self.write_back_button = QPushButton("回写结果到 Excel") self.stop_update_button.setEnabled(False) self.write_back_button.setEnabled(False) @@ -1186,7 +1241,6 @@ if QT_IMPORT_ERROR is None: action_layout.addWidget(self.preview_update_button) action_layout.addWidget(self.start_update_button) action_layout.addWidget(self.stop_update_button) - action_layout.addWidget(self.reset_update_button) action_layout.addStretch(1) action_layout.addWidget(self.write_back_button) @@ -1209,6 +1263,7 @@ if QT_IMPORT_ERROR is None: self.start_update_button.clicked.connect(self.start_update) self.stop_update_button.clicked.connect(self.stop_update) self.reset_update_button.clicked.connect(self.reset_apply_status) + self.task_table.customContextMenuRequested.connect(self.show_task_context_menu) self.write_back_button.clicked.connect(self.write_back_results) self.refresh_tasks() @@ -1273,18 +1328,18 @@ if QT_IMPORT_ERROR is None: dry_run = bool(dry_run) safety_error = self._update_safety_error(tasks, dry_run=dry_run) if safety_error: - QMessageBox.warning(self, "更新安全开关", safety_error) + self._show_update_safety_error(safety_error) self._set_status(safety_error.replace("\n", " ")) return answer = QMessageBox.question( self, - "确认预览本轮更新" if dry_run else "确认开始更新", + "确认检查本轮更新" if dry_run else "确认开始更新", self._confirmation_message(tasks, dry_run=dry_run), QMessageBox.Yes | QMessageBox.No, QMessageBox.No, ) if answer != QMessageBox.Yes: - self._set_status("已取消预览本轮更新" if dry_run else "已取消开始更新") + self._set_status("已取消检查本轮更新" if dry_run else "已取消开始更新") return batch_size = max(1, int(update_cfg.get("max_items_per_run", 1) or 1)) worker = ApplyWorker( @@ -1313,7 +1368,7 @@ if QT_IMPORT_ERROR is None: self._set_apply_running(True) self.run_log_view.clear() if dry_run: - self._set_status(f"开始预览本轮更新:{len(tasks)} 条") + self._set_status(f"开始检查本轮更新:{len(tasks)} 条") else: self._set_status(f"开始更新:{len(tasks)} 条,按每批最多 {batch_size} 条执行") thread.start() @@ -1330,6 +1385,20 @@ if QT_IMPORT_ERROR is None: return self._start_result_write_back(batch_ids, auto=False) + def show_task_context_menu(self, position): + index = self.task_table.indexAt(position) + if index.isValid(): + self.task_table.setCurrentIndex(index) + menu = QMenu(self) + reset_action = menu.addAction("重置更新状态") + reset_action.setEnabled( + self.apply_thread is None + and self.result_write_back_thread is None + and self._selected_task() is not None + ) + reset_action.triggered.connect(self.reset_apply_status) + menu.exec(self.task_table.viewport().mapToGlobal(position)) + def reset_apply_status(self, checked=False): if self.apply_thread is not None or self.result_write_back_thread is not None: self._set_status("更新或回写正在进行,不能重置") @@ -1490,7 +1559,7 @@ if QT_IMPORT_ERROR is None: else "关闭" ) intro = ( - "即将预览当前筛选结果。\n\n" + "即将检查当前筛选结果。\n\n" if dry_run else "即将按当前筛选结果分批更新 Shopee 线上商品。\n\n" ) @@ -1508,7 +1577,7 @@ if QT_IMPORT_ERROR is None: + f"成功后关闭新页={close_text}," + f"多账号并行={parallel_text}\n\n" + ( - "预览只写运行日志,不打开 Shopee、不点击「更新」、不改任务状态。" + "检查只写运行日志,不打开 Shopee、不点击「更新」、不改任务状态。" if dry_run else f"确认后会打开商品编辑页、替换标题/允许时替换封面,并按每批最多 {batch_size} 条点击「更新」提交线上;点击停止后不再开始下一条或下一批。" ) @@ -1519,7 +1588,10 @@ if QT_IMPORT_ERROR is None: if dry_run: return None if not update_cfg.get("allow_real_submit", False): - return "设置未开启「允许真实提交线上商品」,已阻止本次更新。" + return ( + "设置未开启「允许真实提交线上商品」,已阻止本次更新。\n" + "请到⑤设置 > Shopee 更新安全开启该开关后再开始更新。" + ) if not update_cfg.get("allow_cover_update", False): cover_tasks = [ str(getattr(task, "item_id", "")) @@ -1527,9 +1599,25 @@ if QT_IMPORT_ERROR is None: if getattr(task, "new_cover_path", None) ] if cover_tasks: - return "设置未开启「允许更新封面」,当前任务包含新封面路径,已阻止本次更新。" + return ( + "设置未开启「允许更新封面」,当前任务包含新封面路径,已阻止本次更新。\n" + "请到⑤设置 > Shopee 更新安全开启该开关,或先筛掉含新封面的任务。" + ) return None + def _show_update_safety_error(self, message): + box = QMessageBox(self) + box.setIcon(QMessageBox.Warning) + box.setWindowTitle("更新安全开关") + box.setText(str(message)) + settings_button = None + if self.open_settings_callback is not None: + settings_button = box.addButton("前往设置", QMessageBox.ActionRole) + box.addButton(QMessageBox.Ok) + box.exec() + if settings_button is not None and box.clickedButton() is settings_button: + self.open_settings_callback() + def _shopee_update_config(self): defaults = appconfig.default_config().get("shopee_update", {}) loaded = self.config.get("shopee_update", {}) @@ -1593,7 +1681,7 @@ if QT_IMPORT_ERROR is None: self._show_apply_blocked(payload) return self.last_apply_summary = dict(payload) - prefix = "预览本轮更新完成:" if payload.get("dry_run") else "更新完成:" + prefix = "检查本轮更新完成:" if payload.get("dry_run") else "更新完成:" message = prefix + self._apply_progress_text(payload) batch_ids = payload.get("batch_ids") or self._active_batch_ids() if (not payload.get("dry_run")) and payload.get("done", 0) > 0 and batch_ids: @@ -1781,14 +1869,14 @@ if QT_IMPORT_ERROR is None: def _show_apply_summary(self, apply_summary, write_back_payload=None): QMessageBox.information( self, - "预览本轮更新完成" if apply_summary.get("dry_run") else "更新完成", + "检查本轮更新完成" if apply_summary.get("dry_run") else "更新完成", self._apply_summary_message(apply_summary, write_back_payload), ) def _apply_summary_message(self, apply_summary, write_back_payload=None, error=None): dry_run = bool(apply_summary.get("dry_run")) lines = [ - "预览本轮更新完成,未打开 Shopee、未提交线上、未改任务状态。" + "检查本轮更新完成,未打开 Shopee、未提交线上、未改任务状态。" if dry_run else "更新完成。", "{success_label}:{applied},失败:{failed},略过:{skipped}".format( @@ -2718,7 +2806,7 @@ if QT_IMPORT_ERROR is None: self._run_id = self._create_run_log(eligible, batch_ids) self._log_run_event( "运行开始:{mode},任务{total},每批最多{batch_size},批次{batch_count},{parallel}".format( - mode="预览本轮更新" if self.dry_run else "真实更新", + mode="检查本轮更新" if self.dry_run else "真实更新", total=total, batch_size=batch_size, batch_count=len(batches), @@ -2856,7 +2944,7 @@ if QT_IMPORT_ERROR is None: def _log_batch_start(self, batch_index, batch_count, batch_tasks, counters, total): first = counters["done"] + 1 last = min(first + len(batch_tasks) - 1, total) - label = "预览批次" if self.dry_run else "更新批次" + label = "检查批次" if self.dry_run else "更新批次" self._log_run_event( f"{label} {batch_index}/{batch_count} 开始:任务 {first}-{last}/{total}" ) @@ -2905,7 +2993,7 @@ if QT_IMPORT_ERROR is None: if account is None: reason = "别名未匹配账号" self._log_run_event( - f"预览:任务 {task.id} 商品 {task.item_id} 将略过:{reason}", + f"检查:任务 {task.id} 商品 {task.item_id} 将略过:{reason}", task=task, level="warning", ) @@ -2917,7 +3005,7 @@ if QT_IMPORT_ERROR is None: action_parts.append("封面") action_text = "+".join(action_parts) or "无变更" self._log_run_event( - "预览:任务 {task_id} 商品 {item_id} 账号 {alias} 将更新 {action}".format( + "检查:任务 {task_id} 商品 {item_id} 账号 {alias} 将更新 {action}".format( task_id=task.id, item_id=task.item_id, alias=account.alias, @@ -3773,7 +3861,7 @@ if QT_IMPORT_ERROR is None: self.max_items_per_run_spin.setToolTip("作为每批最大更新条数;正式更新会分批处理当前筛选全部可更新记录。") self.close_success_tab_checkbox = QCheckBox("成功后关闭本次新开编辑页") self.close_success_tab_checkbox.setObjectName("closeSuccessTabCheckbox") - self.dry_run_checkbox = QCheckBox("预览本轮更新") + self.dry_run_checkbox = QCheckBox("检查本轮更新") self.dry_run_checkbox.setObjectName("dryRunCheckbox") self.dry_run_checkbox.setVisible(False) self.parallel_accounts_checkbox = QCheckBox("多账号并行更新") @@ -4737,6 +4825,7 @@ if QT_IMPORT_ERROR is None: config=self.config, status_callback=self.statusBar().showMessage, open_accounts_callback=lambda: self.open_accounts_tab(), + open_settings_callback=lambda: self.open_settings_tab(), ) if title == "④ 账号管理": return AccountsTab( @@ -4762,6 +4851,9 @@ if QT_IMPORT_ERROR is None: def open_accounts_tab(self): self.tabs.setCurrentIndex(TAB_TITLES.index("④ 账号管理")) + + def open_settings_tab(self): + self.tabs.setCurrentIndex(TAB_TITLES.index("⑤ 设置")) else: class MainWindow(QMainWindow): def __init__(self): diff --git a/docs/00-ai-start-here.md b/docs/00-ai-start-here.md index 04acea8..d209e4d 100644 --- a/docs/00-ai-start-here.md +++ b/docs/00-ai-start-here.md @@ -67,7 +67,7 @@ cmshopee 是一个 Windows 本地桌面自动化工具(PySide6,5 Tab), - 5 Tab 流水线:① 导入采集 → ② AI生成 → ③ 更新shopee → ④ 账号管理 → ⑤ 设置。 - GUI 固定为 PySide6;后台采集/生成/更新用 `QObject` worker + `QThread` + signal 回传进度。 -- 多账号管理;账号以独立 user-data-dir 隔离。③ 更新默认串行,提供「预览本轮更新」按钮;⑤ 可开启按账号并行和设置每批最大更新条数。 +- 多账号管理;账号以独立 user-data-dir 隔离。③ 更新默认串行,提供「检查本轮更新」按钮;⑤ 可开启按账号并行和设置每批最大更新条数。 - Excel 导入/回写 + SQLite 实时落库 + 本地图片目录。 - AI 生成标题/封面,不设逐条确认阶段。 - ③ 点击「开始更新」后弹窗确认当前筛选范围和任务数量;用户确认后才批量提交线上。 diff --git a/docs/02-requirements.md b/docs/02-requirements.md index cef31a3..ad8f1cd 100644 --- a/docs/02-requirements.md +++ b/docs/02-requirements.md @@ -45,8 +45,8 @@ | AI 生成(②) | 输入标题/封面提示词,AI 据提示词+旧标题生成新标题、据提示词+旧封面生成新封面 | P0 | | 提示词管理(②) | 标题提示词「保存」到 `title_prompt.txt` 并启动回显;封面提示词多模板(下拉 + 新建/保存/另存为/重命名/删除)+ 插入 `{新标题}` + 预览(变量替换) | P0 | | 查看对照(②) | 双击任务弹窗查看新旧封面(纯查看,无逐条审核阶段);可选对单行重生成 | P0 | -| 更新 shopee(③) | 按批次/店铺/状态筛选;可先「预览本轮更新」确认范围;点击「开始更新」后弹窗确认,确认后对当前筛选出的已生成任务按每批最大条数分批更新 Shopee,逐条换标题+封面并点「更新」提交线上;普通正式更新不再受测试商品 ID 限制,可批量更新真实商品;可按状态=失败重试 | P0 | -| 预览本轮更新 / 运行日志 / 多账号并行(③/⑤) | ③ 提供「预览本轮更新」按钮,不打开 Shopee、不提交、不改任务状态,只显示当前筛选范围、店铺分布、预计分批和会更新字段;真实更新写运行日志;可在⑤开启多账号并行,同账号内仍串行 | V2 已接入,预览按钮待改名接入 | +| 更新 shopee(③) | 按批次/店铺/状态筛选;可先「检查本轮更新」确认范围;点击「开始更新」后弹窗确认,确认后对当前筛选出的已生成任务按每批最大条数分批更新 Shopee,逐条换标题+封面并点「更新」提交线上;普通正式更新不再受测试商品 ID 限制,可批量更新真实商品;可按状态=失败重试 | P0 | +| 检查本轮更新 / 运行日志 / 多账号并行(③/⑤) | ③ 提供「检查本轮更新」按钮,不打开 Shopee、不提交、不改任务状态,只显示当前筛选范围、店铺分布、预计分批和会更新字段;真实更新写运行日志;可在⑤开启多账号并行,同账号内仍串行 | V2 已接入,检查按钮已接入 | | 结果存储与回写 | 各阶段结果实时存 SQLite;该文件全部完成后把旧/新数据+状态批量回写原 Excel | P0 | | 设置(⑤) | AI 模型管理(下拉+新增/删除/详情/测试连接,至少各一个文本+图像模型);标题/图片大模型角色选择;分辨率(512/1k/2k/4k,返回超时随分辨率自动);并发/重试/jpg质量;图片目录/Chrome 路径/端口;设置页采用居中内容区、适度左右留白和响应式三列表单,长字段跨列显示;点击「保存设置」成功后弹轻量提示框 | P0 | | 首次引导保护 | 未配账号、对应账号 Chrome 未启动或未登录时,① ③ 执行按钮禁用/执行前拦截并提示去④;③ 不自动打开缺失账号 Chrome,必须中止本轮更新 | P0 | @@ -107,8 +107,8 @@ - **AI Key 安全**:Key 本地明文存于 `config/ai_models.json`,保存/变更时提示,UI 打码,不写日志,不提交版本库。 - **第三方平台风险**:Shopee 页面结构、class 名、接口随时可能变;限流、风控、封号风险存在,禁止高频批量。 - **自动化边界风险**:③ 确认后会自动改标题、上传图片、拖拽并点「更新」提交线上;点「开始更新」并确认前需自行确保筛选范围、任务来源与 AI 产出可接受。 -- **预览边界**:③「预览本轮更新」只预览当前筛选任务并写运行日志,不打开 Shopee、不点击更新、不改任务状态;点击「开始更新」并确认后才可能真实提交。 +- **检查边界**:③「检查本轮更新」只检查当前筛选任务并写运行日志,不打开 Shopee、不点击更新、不改任务状态;点击「开始更新」并确认后才可能真实提交。 - **多账号并行风险**:⑤ 开启多账号并行后,不同账号可同时执行更新;同一账号内仍串行。执行前检查本轮账号调试端口,端口冲突时阻断真实更新。 - **合规风险**:仅在自有/授权账号上操作;遵守 Shopee 卖家条款;不绕过任何平台限制。 - **GUI 选型**:V1 固定为 PySide6;后台任务通过 QThread/signal 回传进度,见 [技术栈](03-tech-stack.md) 与 [架构](04-architecture.md)。 -- **多账号并行默认关闭**:默认仍串行;只在⑤明确开启后并行,建议先用「预览本轮更新」确认账号、店铺和任务范围。 +- **多账号并行默认关闭**:默认仍串行;只在⑤明确开启后并行,建议先用「检查本轮更新」确认账号、店铺和任务范围。 diff --git a/docs/04-architecture.md b/docs/04-architecture.md index 7ecb465..15de877 100644 --- a/docs/04-architecture.md +++ b/docs/04-architecture.md @@ -137,7 +137,7 @@ imported → collected → generated → applied - `allow_cover_update`:是否允许本次更新包含新封面路径;默认 `false`,未开启时只允许标题更新任务。 - `max_items_per_run`:每批最大更新任务数;默认 `1`,当前筛选结果超过该值时自动分批,不再按总数阻断。 - `close_success_tab`:成功提交后是否关闭本轮程序自动新开的商品编辑页;默认 `false`。只关闭 `open_product()` 本轮新建且已提交成功的 tab,失败任务和用户原本打开的 tab 保留。Shopee 确认成功并跳回商品列表页后,关闭前等待 2 秒,让列表页跳转和页面状态稳定。 -- `dry_run`:内部兼容字段;普通用户界面不展示该开关,③「预览本轮更新」按钮触发预览,只写运行日志,不打开 Shopee、不点击「更新」、不改任务状态;默认 `false`。 +- `dry_run`:内部兼容字段;普通用户界面不展示该开关,③「检查本轮更新」按钮触发检查模式,只写运行日志,不打开 Shopee、不点击「更新」、不改任务状态;默认 `false`。 - `parallel_accounts`:是否按账号并行执行③真实更新;默认 `false`,即保持串行。 - `max_parallel_accounts`:最多同时执行的账号数;同一账号内仍按任务串行,默认 `2`。 @@ -388,7 +388,7 @@ images///__new. # AI 生成的新封 ### 6.3 应用更新(③ Tab) - ③ 顶部筛选确定本次作用范围;点击「开始更新」后弹窗展示筛选条件、任务数量和“将提交线上”的风险提示。 -- ③ 提供「预览本轮更新」按钮:只读取当前筛选结果和写运行日志,不打开 Shopee、不提交、不改任务状态;预览展示总数、店铺分布、每批最大条数、预计批次数、更新字段和略过原因。 +- ③ 提供「检查本轮更新」按钮:只读取当前筛选结果和写运行日志,不打开 Shopee、不提交、不改任务状态;检查汇总展示总数、店铺分布、每批最大条数、预计批次数、更新字段和略过原因。 - 弹确认前先读取 `config.json` 的 `shopee_update`:未开启 `allow_real_submit`,或任务含新封面但未开启 `allow_cover_update` 时,直接弹警告阻断,不创建真实更新 `ApplyWorker`。普通正式更新不再检查 `test_item_id`,当前筛选结果可以包含多个真实商品 ID。`max_items_per_run` 作为每批最大任务数,当前筛选总数超过该值时自动分批,不再按总数阻断。 - 用户点「是/确认」才开始批量更新;点「否/取消」不执行、不改库。 - 真实更新前必须做账号就绪预检:按当前筛选结果汇总需要的账号;无账号、账号 Chrome 未启动、CDP 端口不可访问、未登录或本轮账号端口冲突时,整体返回 `blocked` 并由 GUI 弹窗列出账号/原因、引导去④账号管理。预检不通过时不创建商品编辑页、不调用 `editor.apply_task()`、不写失败状态、不自动调用「启动登录」或静默打开 Chrome。 @@ -396,8 +396,8 @@ images///__new. # AI 生成的新封 - `click_update` 点击页面「更新」后必须处理 Shopee 站点侧二次确认框。2026-06-29 真实测试商品实测:页面会出现 `.eds-modal__content` / `.eds-modal__box`,标题为 `確定您要更新商品嗎?`,正文提示建议优化,底部两个按钮分别是 `立即優化` 与主按钮 `更新`。实现时只允许在标题匹配该确认框、且按钮位于可见 modal footer 内时点击 `button.eds-button--primary` / 文案 `更新`;不得点击 `立即優化`。如果弹窗出现但未成功点击主按钮,当前任务必须视为未提交失败,不得写 `committed=1`。 - 更新封面时,`replace_cover()` 必须按“替换第一张”语义执行:无论当前商品图片是 8 张还是 9 张,只要本次有新封面,就先确认该任务已有本地旧封面备份(`old_cover_path` 非空且文件存在),再删除当前线上第一张图、等待图片管理器稳定、上传新图并拖到第一位。缺失备份时不删除线上第一张图,直接返回明确错误,要求先回到①采集旧封面或修复本地备份。 - 默认串行、单条失败继续;⑤ 开启 `parallel_accounts` 后按账号分组并行,不同账号可同时跑,同一账号内仍串行。真实更新前检查本轮账号 `debug_port`,端口冲突直接阻断。 -- 「预览本轮更新」只写 `run_logs/run_log_events` 和弹窗/状态栏预览,不调用 `editor.apply_task()`,不做账号登录预检,不写任务状态,不回写 Excel。 -- 真实更新按每批最大条数分批执行,每条立即写 SQLite;全部完成回写 Excel(新字段+状态)+ 弹窗汇总。真实更新与预览都写运行日志,日志 payload 走脱敏工具。 +- 「检查本轮更新」只写 `run_logs/run_log_events` 和弹窗/状态栏检查汇总,不调用 `editor.apply_task()`,不做账号登录预检,不写任务状态,不回写 Excel。 +- 真实更新按每批最大条数分批执行,每条立即写 SQLite;全部完成回写 Excel(新字段+状态)+ 弹窗汇总。真实更新与检查都写运行日志,日志 payload 走脱敏工具。 - 分批更新停止语义为协作式停止:点击停止后设置取消标记;当前正在执行的商品跑到安全边界后写库结束,不再开始新商品,也不进入下一批。未开始任务保持原状态,后续可继续。 - T-404a 已在③提供「重置更新状态」:仅当前选中单条,保留 `new_title/new_cover_path`,本地退回 `stage=generated/status=pending` 以便重复测试上传/提交;若 `committed=1`,必须提示线上已提交过、本地重置不回滚 Shopee、重复更新会再次提交,并保留 committed 历史事实/运行日志。 - 若 `close_success_tab=true`,且商品页是本轮程序自动新建、并已成功提交,则提交后关闭该商品编辑页;若确认后已跳回商品列表页,关闭前等待 2 秒;失败任务和复用的用户已有 tab 不关闭。 diff --git a/docs/06-tasks.md b/docs/06-tasks.md index 5e8e241..bdeaf8e 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -89,8 +89,11 @@ | 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-505 | 全流程诊断日志扩展 | T-207, T-504 | 在 T-207 采集日志机制稳定后,扩展到② AI生成、Excel 导入/回写、③ 更新shopee步骤级日志、④ Chrome 启动/登录检测、⑤ AI模型测试连接;数据库日志保存业务可读步骤和错误,本地 log 保存脱敏 traceback;不得记录 Cookie、密码、API Key、token | TODO | -| T-506 | 正式使用批量更新体验:③ 预览本轮更新 + 分批更新全部 + ⑤ 设置页三列布局 | T-501c, T-504 | ③ 增加「预览本轮更新」按钮替代用户可见 dry-run;「开始更新」对当前筛选结果按每批最大更新条数自动分批,确认弹窗显示总数/每批/预计批次,停止为当前商品安全结束后不再开始新任务;⑤ 设置页使用居中内容区,左右留白已缩短为 T-506 初始实现约 40%,模型详情/角色与生成参数/路径与端口/Shopee 更新安全均使用响应式三列布局,长字段跨列;「多账号并行更新」与「最大并行账号数」合并为同一个横向组件,最大并行账号数紧跟其后且不换行 | DONE | +| T-506 | 正式使用批量更新体验:③ 检查本轮更新 + 分批更新全部 + ⑤ 设置页三列布局 | T-501c, T-504 | ③ 增加「检查本轮更新」按钮替代用户可见 dry-run;「开始更新」对当前筛选结果按每批最大更新条数自动分批,确认弹窗显示总数/每批/预计批次,停止为当前商品安全结束后不再开始新任务;⑤ 设置页使用居中内容区,左右留白已缩短为 T-506 初始实现约 40%,模型详情/角色与生成参数/路径与端口/Shopee 更新安全均使用响应式三列布局,长字段跨列;「多账号并行更新」与「最大并行账号数」合并为同一个横向组件,最大并行账号数紧跟其后且不换行 | 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-509 | ② 新标题人工微调 | T-303, T-401 | ②任务表「新标题」列可编辑;仅允许已生成、未提交线上、非运行中任务;编辑写回 `tasks.new_title`,保留 `stage=generated`,清空 `last_error`,状态回到可更新;不触碰 Shopee/CDP/Excel,③ 使用修改后的标题 | DONE | +| T-510 | ③ 检查本轮更新文案统一 | T-506, T-508 | 将用户可见「预览本轮更新」统一改为「检查本轮更新」,按钮、确认框、状态栏、完成弹窗、运行日志和文档一致;保留内部 `dry_run` 字段与执行语义,不改 Shopee/CDP 更新流程 | DONE | ## 里程碑 diff --git a/docs/api.md b/docs/api.md index 6b2cd09..ca9c0ab 100644 --- a/docs/api.md +++ b/docs/api.md @@ -92,6 +92,7 @@ mark_failed(task_id, phase, error) -> None # status=failed,stage 不前进 mark_skipped(task_id, reason) -> None # status=skipped,stage 不前进 set_collected(task_id, old_title, old_cover_path) -> None # stage=collected,status=success,collect_attempts+1 set_generated(task_id, new_title, new_cover_path) -> None # stage=generated,status=success,generate_attempts+1 +update_generated_title(task_id, new_title, path=None) -> None # T-509 本地人工微调新标题;仅 generated/未提交/非运行中;清空 last_error 并回到 pending,不触碰 Shopee/CDP/Excel/封面 set_applied(task_id, committed, error=None) -> None # 成功 stage=applied;失败 status=failed 且 stage 不前进 # T-404a 已实现:选中记录重置 reset_generated(task_id, delete_file=False) -> dict # 本地清空 AI 结果并退回 collected;默认不删新封面文件 @@ -328,23 +329,23 @@ main() -> int # 创建 QApplication + MainWindow class MainWindow(QMainWindow) # QTabWidget: ①②③④⑤;支持注入 db_path/config/config_path/ai_models_path 便于测试 class CollectTab(QWidget) # ① 导入采集:导入 Excel + 汇总栏 + QTableView 任务列表 + 未匹配略过标记 class GenerateTab(QWidget) # ② AI生成:提示词管理 + 筛选任务 + 开始/停止生成 + 新旧封面预览 + AI生成运行日志 -class ApplyTab(QWidget) # ③ 更新shopee:筛选已生成任务 + 预览本轮更新 + 安全开关拦截 + 确认后分批真实更新 + 运行日志 +class ApplyTab(QWidget) # ③ 更新shopee:筛选已生成任务 + 检查本轮更新 + 安全开关拦截 + 确认后分批真实更新 + 运行日志 class SettingsTab(QWidget) # ⑤ 设置:AI 模型管理 + 响应式三列布局 + 角色/生成参数/路径端口 + Shopee 更新安全 class CollectWorker(BaseWorker) # ① 后台采集:账号就绪预检 -> editor.collect -> db.set_collected/mark_skipped/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 class WriteBackWorker(BaseWorker) # ①/③ 后台回写:旧字段或更新结果写回原 Excel class AIModelTestWorker(BaseWorker) # ⑤ 后台测试 AI 模型连接:appconfig.test_ai_model class TaskTableModel(QAbstractTableModel) # 任务表格模型:账号/别名/商品ID/阶段;未匹配别名显示“略过” -class GenerateTaskTableModel(QAbstractTableModel) # ② 任务表格模型:店铺/商品ID/旧标题/新标题/状态 -class ApplyTaskTableModel(QAbstractTableModel) # ③ 任务表格模型:店铺/商品ID/新标题/新封面/阶段/结果 +class GenerateTaskTableModel(QAbstractTableModel) # ② 任务表格模型:店铺/商品ID/旧标题/新标题/状态;generated/未提交/非运行中新标题可本地编辑 +class ApplyTaskTableModel(QAbstractTableModel) # ③ 任务表格模型:店铺/商品ID/新标题/新封面/阶段/结果;保持只读,重置更新状态走右键菜单 class AccountsTab(QWidget) # ④ 账号管理:表格 + 增删改 + 启动登录 + 检测登录 + 快捷方式 class AccountDialog(QDialog) # 账号编辑弹窗;密码 QLineEdit.Password TAB_TITLES: list[str] # 固定 Tab 顺序 TAB_STYLE: str # 顶层 Tab 栏防误点样式:最小宽度/padding/间距/当前态 ``` -`MainWindow` 已实现五 Tab、① 导入采集任务列表、② AI生成布局/提示词/开始生成/停止/封面对照预览、③ 更新shopee筛选列表与预览/确认后分批真实更新、④ 账号管理、⑤ AI 模型管理。缺 PySide6 时 `main()` 返回 1 并输出明确提示。 +`MainWindow` 已实现五 Tab、① 导入采集任务列表、② AI生成布局/提示词/开始生成/停止/封面对照预览、③ 更新shopee筛选列表与检查/确认后分批真实更新、④ 账号管理、⑤ AI 模型管理。缺 PySide6 时 `main()` 返回 1 并输出明确提示。 主 Tab 栏必须在 `MainWindow` 初始化时应用 `TAB_STYLE`:5 个 Tab 不使用 Qt 默认紧凑宽度,需保证点击区域稳定、间距清晰、当前 Tab 高亮明显。该样式属于全局导航基础,不归后续业务 Tab 任务重复实现。 @@ -365,7 +366,7 @@ TAB_STYLE: str # 顶层 Tab 栏防误点样式: - 角色与生成参数读写 `config.json`,并按 3 个组件一组排列:标题大模型(仅 text)、图片大模型(仅 image)、标题/图片并发、失败重试、分辨率、jpg 质量。 - 分辨率下拉固定 `512/1k/2k/4k`;返回超时标签只读展示 `resolution_timeouts[resolution]`。 - 路径与端口读写 `config.json`,并按 3 个组件一组排列:默认调试端口、调试端口范围、CDP 就绪超时等短字段一格;Chrome 路径、账号数据根目录、图片目录、DB 路径等长字段跨整行或跨 2/3 列。保存时校验端口范围和默认端口。 -- Shopee 更新安全读写 `config.json` 的 `shopee_update` 段,并按 3 个组件一组排列:允许真实提交、允许更新封面、每批最大更新条数、成功后关闭本次新开编辑页、多账号并行、最大并行账号数;其中「多账号并行更新」与「最大并行账号数」必须合并为同一个横向组件,最大并行账号数紧跟在多账号并行更新后面,不允许被三列表单排到下一行。`dry_run` 字段可保留为内部兼容,但普通用户界面不再展示 dry-run 开关,③ 使用「预览本轮更新」按钮触发预览;测试商品 ID 仅作为历史/调试兼容字段保留,不参与普通正式更新安全检查,普通设置页已隐藏该入口。 +- Shopee 更新安全读写 `config.json` 的 `shopee_update` 段,并按 3 个组件一组排列:允许真实提交、允许更新封面、每批最大更新条数、成功后关闭本次新开编辑页、多账号并行、最大并行账号数;其中「多账号并行更新」与「最大并行账号数」必须合并为同一个横向组件,最大并行账号数紧跟在多账号并行更新后面,不允许被三列表单排到下一行。`dry_run` 字段可保留为内部兼容,但普通用户界面不再展示 dry-run 开关,③ 使用「检查本轮更新」按钮触发检查模式;测试商品 ID 仅作为历史/调试兼容字段保留,不参与普通正式更新安全检查,普通设置页已隐藏该入口。 - 真实提交与封面更新默认关闭;用户在 ⑤ 保存开启后,③ 仍需要通过安全开关检查并弹窗确认,才会创建更新 worker。 ① 导入采集当前要点(T-202/T-202b): @@ -389,8 +390,8 @@ TAB_STYLE: str # 顶层 Tab 栏防误点样式: - 封面提示词模板下拉读取 `prompts/cover/*.txt`;支持新建、保存、另存为、重命名、删除。删除由 GUI 二次确认,删空后下拉显示内存态“默认”,不会自动建文件。 - 「插入标题」在封面提示词光标处插入 `{新标题}`;「预览」使用当前选中任务(无选择则用第一条)调用 `prompts.render_prompt()` 并弹窗展示。 - 筛选栏包含:批次、店铺、商品ID、状态、刷新。批次来自 `db.list_batches()`;店铺来自当前任务别名并优先显示匹配账号名;商品ID输入框按包含匹配 `item_id`,清空表示全部;状态支持全部/待生成/已生成/失败/略过/已更新。 -- 任务列表使用 `QTableView + GenerateTaskTableModel`,列为:店铺、商品ID、旧标题、新标题、状态。`stage=collected` 显示“待生成”,`stage=generated` 显示“已生成”,`status=failed/skipped/running` 优先显示对应状态。 -- 双击任务行弹窗展示旧封面与新封面路径对应图片;图片不存在时显示空态/路径提示,只做查看,不做审核。 +- 任务列表使用 `QTableView + GenerateTaskTableModel`,列为:店铺、商品ID、旧标题、新标题、状态。`stage=collected` 显示“待生成”,`stage=generated` 显示“已生成”,`status=failed/skipped/running` 优先显示对应状态;已生成、未提交线上、非运行中的「新标题」列可双击编辑,调用 `db.update_generated_title()` 写回本地并清空 `last_error`。 +- 双击「新标题」列进入本地编辑;双击其他列弹窗展示旧封面与新封面路径对应图片;图片不存在时显示空态/路径提示,只做查看,不做审核。 - 底部「开始生成」只处理当前筛选结果里 `stage=collected` 的任务;批次/店铺/商品ID/状态筛选共同决定当前筛选结果;通过 `GenerateWorker` 调 `ai.generate_batch()`,先并发标题、再并发封面。 - 「停止」调用 worker 的协作式 `cancel()`;未开始的 Future 取消,不记失败;已完成的 `db.set_generated()` 结果保留,可再次开始生成剩余任务。 - 进度标签显示:`标题x/n · 封面x/n · 失败z`;每条封面生成成功后立即 `db.set_generated()`,失败 `db.mark_failed(..., "generate", error)`,GUI 刷新任务表。 @@ -402,18 +403,18 @@ TAB_STYLE: str # 顶层 Tab 栏防误点样式: - ③ 只列出已具备新标题/新封面、可进入更新阶段的任务:`stage=generated/applied`,或已有新字段且 `status=failed/skipped` 的任务。 - 状态筛选支持:已生成(默认,`stage=generated` 且 `status=success/pending`)、失败、已更新、略过、全部状态。 - 任务列表使用 `QTableView + ApplyTaskTableModel`,列为:店铺、商品ID、新标题、新封面、阶段、结果。 -- 「开始更新」只读取当前筛选结果;无任务时只提示,不弹确认、不改库。 -- 「预览本轮更新」只读取当前筛选结果并创建预览运行日志,不打开 Shopee、不调用 `editor.apply_task()`、不写任务状态、不回写 Excel;预览内容包含任务总数、店铺分布、每批最大条数、预计批次数、会更新字段和略过原因。 -- 点击「开始更新」先读取 `shopee_update`:未允许真实提交,或包含新封面但未允许封面更新时,弹警告并阻断;普通正式更新不再检查 `test_item_id`,当前筛选结果可包含多个真实商品 ID;`max_items_per_run` 作为每批最大更新条数,当前筛选结果超过该值时自动分批。通过后才弹窗展示批次/店铺/商品ID/状态/任务总数、每批最大条数、预计批次数、提交线上风险和当前安全设置。 +- 「开始更新」只读取当前筛选结果;无任务时只提示,不弹确认、不改库;该按钮是③的主操作,视觉上强于检查、停止和回写。 +- 「检查本轮更新」只读取当前筛选结果并创建检查运行日志,不打开 Shopee、不调用 `editor.apply_task()`、不写任务状态、不回写 Excel;检查内容包含任务总数、店铺分布、每批最大条数、预计批次数、会更新字段和略过原因。 +- 点击「开始更新」先读取 `shopee_update`:未允许真实提交,或包含新封面但未允许封面更新时,弹警告并阻断;拦截弹窗写明具体设置项并提供「前往设置」跳到⑤;普通正式更新不再检查 `test_item_id`,当前筛选结果可包含多个真实商品 ID;`max_items_per_run` 作为每批最大更新条数,当前筛选结果超过该值时自动分批。通过后才弹窗展示批次/店铺/商品ID/状态/任务总数、每批最大条数、预计批次数、提交线上风险和当前安全设置。 - 用户点否/取消时不执行、不改库;用户点是后才创建 `ApplyWorker` 做真实提交。 - `ApplyWorker` 只处理当前筛选结果里 `stage=generated` 且已有新标题或新封面、状态为 `success/pending/failed` 的任务;已更新和略过记录仅查看,不会再次提交,除非用户先用 T-404a 的「重置更新状态」把选中记录退回可更新。 -- 预览本轮更新:不做账号登录预检,不调用 `editor.apply_task()`,不写任务状态,不回写 Excel;只把每条“将更新/将略过”写入运行日志并弹汇总。 +- 检查本轮更新:不做账号登录预检,不调用 `editor.apply_task()`,不写任务状态,不回写 Excel;只把每条“将更新/将略过”写入运行日志并弹汇总。 - 真实更新前先做账号就绪预检:无账号、当前筛选结果匹配账号 Chrome 未启动、CDP 端口不可访问、未登录,或本轮涉及账号调试端口冲突时,返回 `blocked=True`,GUI 弹窗汇总并跳转/引导去④账号管理;预检不通过时不调用 `editor.apply_task()`、不写失败状态、不自动启动 Chrome。 - 预检通过后默认串行;若 `parallel_accounts=true` 且 `max_parallel_accounts>1`,按账号分组并行执行,不同账号可同时跑,同一账号内仍串行。每条执行 `db.mark_running(..., "apply")` → `editor.apply_task(account, task, close_success_tab=设置值)` → `db.set_applied()`;成功推进 `stage=applied/status=success/committed=1`,失败保持原 stage、`status=failed/committed=0/last_error`,单条失败继续下一条。 -- 预览和真实更新都会创建 `run_logs`,并把逐条事件写入 `run_log_events`;③ 页面显示最近运行日志。 +- 检查和真实更新都会创建 `run_logs`,并把逐条事件写入 `run_log_events`;③ 页面显示最近运行日志。 - 若 `close_success_tab=true`,`editor.apply_task()` 只关闭本轮自动新开且成功提交的商品页;确认后跳回商品列表页时,关闭前等待 2 秒;失败和复用的用户已有 tab 不关闭。 - 别名未匹配账号的任务逐条 `db.mark_skipped()`,原因 `别名未匹配账号`;「停止」调用 worker 协作式 `cancel()`,已开始单条跑到安全边界后结束。 -- T-404a 已实现:「重置更新状态」读取当前选中单条;确认后保留 `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` 时必须提示线上已提交过且不回滚 Shopee,并保留 committed 历史事实/运行日志。 - ③ 没有常驻提交开关;确认弹窗是提交线上前的边界。 - 更新完成后自动调用 `WriteBackWorker(mode="results")` → `excel.write_back_results()`,把新标题、新封面路径、更新状态写回原 Excel;原文件被占用时提示关闭后点击「回写结果到 Excel」手动重试,SQLite 更新结果不回滚。 - 自动结果回写完成后弹窗汇总成功/失败/略过数量与 Excel 回写文件/行数;若没有可回写批次,也会弹出更新汇总。 @@ -450,7 +451,7 @@ run_worker(worker: BaseWorker, thread_name=None, start=True) -> QThread - 采集、AI 生成、更新、Excel 回写都通过 worker 执行,用 signal 回传进度。 - 每个 worker/线程按需创建自己的 SQLite connection,不跨线程共享连接。 - ③ 的批量确认弹窗在 GUI 主线程完成;用户确认后才创建 `ApplyWorker`。 -- `ApplyWorker` 支持预览、默认串行和按账号并行;真实更新按 `max_items_per_run` 分批调用 `editor.apply_task(..., close_success_tab=...)`,逐条 `set_applied()`,失败继续;账号未就绪或端口冲突时整体阻断并引导④,不进入逐条提交,也不静默启动账号 Chrome。点击停止为协作式停止:当前商品完成后不再开始新商品或下一批。 +- `ApplyWorker` 支持检查、默认串行和按账号并行;真实更新按 `max_items_per_run` 分批调用 `editor.apply_task(..., close_success_tab=...)`,逐条 `set_applied()`,失败继续;账号未就绪或端口冲突时整体阻断并引导④,不进入逐条提交,也不静默启动账号 Chrome。点击停止为协作式停止:当前商品完成后不再开始新商品或下一批。 - `WriteBackWorker` 默认 `mode="old"` 回写旧字段;③ 使用 `mode="results"` 回写新标题/新封面/更新状态,支持单批次或多批次列表。 - `execute()` 未捕获异常会发 `failed(-1, error)` 与 `finished({"ok": False, "error": ...})`;普通单行失败由业务 worker 自己发 `failed(task_id, error)` 后继续处理。 diff --git a/docs/current-state.md b/docs/current-state.md index e05a44b..70032c0 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -6,10 +6,10 @@ ## 当前快照 - 日期:2026-07-01 -- 阶段:V0 单账号 CDP 流程已验证;V1 已完成 T-000 正式代码包结构、T-001 `app/editor.py` 模块化、T-002 `app/appconfig.py` 应用配置、T-003 SQLite 持久化地基、T-004 本地数据忽略规则、T-005 AI 模型清单后端、T-006 单元测试基座、T-101 账号 user-data-dir 工具、T-102 Chrome 启动器、T-103 登录保活与检测、T-104 PySide6 主窗口骨架、T-104b PySide6 worker 基座、T-105 Tab④ 账号管理、T-106 账号快捷方式、T-201 Excel 导入入库、T-202 Tab① 任务列表与导入按钮、T-202b Tab① 导入汇总栏、T-203 采集旧标题旧封面、T-204 回写旧字段到原 Excel、T-204b 采集完成自动回写、T-205 首次未配账号/Chrome 未启动/未登录引导保护、T-205b 采集后关闭自动新建商品页 tab、T-206 Tab① 删除指定批次软删除、T-301 AI 生成接口、T-302 Tab② AI 生成布局与任务列表、T-302p 提示词管理、T-303 Tab② 开始生成/停止/进度、T-303b ②/③ 商品ID筛选、T-401 Tab③ 更新列表筛选与开始更新确认、T-402 Tab③ 确认后串行更新、T-403 Tab③ 结果回写与结束汇总、T-501 Tab⑤ AI 模型管理 UI、T-501b Tab⑤ 角色与生成参数、T-501c Tab⑤ Shopee 更新安全开关、T-502 换封面删第一张再上传、T-503 敏感信息本地明文保存提示与日志脱敏、T-504 多账号并行/dry-run/运行日志、T-207 ①采集诊断日志、② AI生成图片失败诊断日志补丁、T-404a ②/③ 选中记录重置、T-404 真实 Shopee 单条更新冒烟验收、T-506 正式使用批量更新体验、T-507 正式批量更新移除普通流程测试商品 ID 限制。 +- 阶段:V0 单账号 CDP 流程已验证;V1 已完成 T-000 正式代码包结构、T-001 `app/editor.py` 模块化、T-002 `app/appconfig.py` 应用配置、T-003 SQLite 持久化地基、T-004 本地数据忽略规则、T-005 AI 模型清单后端、T-006 单元测试基座、T-101 账号 user-data-dir 工具、T-102 Chrome 启动器、T-103 登录保活与检测、T-104 PySide6 主窗口骨架、T-104b PySide6 worker 基座、T-105 Tab④ 账号管理、T-106 账号快捷方式、T-201 Excel 导入入库、T-202 Tab① 任务列表与导入按钮、T-202b Tab① 导入汇总栏、T-203 采集旧标题旧封面、T-204 回写旧字段到原 Excel、T-204b 采集完成自动回写、T-205 首次未配账号/Chrome 未启动/未登录引导保护、T-205b 采集后关闭自动新建商品页 tab、T-206 Tab① 删除指定批次软删除、T-301 AI 生成接口、T-302 Tab② AI 生成布局与任务列表、T-302p 提示词管理、T-303 Tab② 开始生成/停止/进度、T-303b ②/③ 商品ID筛选、T-401 Tab③ 更新列表筛选与开始更新确认、T-402 Tab③ 确认后串行更新、T-403 Tab③ 结果回写与结束汇总、T-501 Tab⑤ AI 模型管理 UI、T-501b Tab⑤ 角色与生成参数、T-501c Tab⑤ Shopee 更新安全开关、T-502 换封面删第一张再上传、T-503 敏感信息本地明文保存提示与日志脱敏、T-504 多账号并行/dry-run/运行日志、T-207 ①采集诊断日志、② AI生成图片失败诊断日志补丁、T-404a ②/③ 选中记录重置、T-404 真实 Shopee 单条更新冒烟验收、T-506 正式使用批量更新体验、T-507 正式批量更新移除普通流程测试商品 ID 限制、T-508 ③ 更新shopee生产化操作区、T-509 ② 新标题人工微调、T-510 ③ 检查本轮更新文案统一。 - 技术栈:Python 3.10+,自研 CDP(websocket-client + requests),SQLite(sqlite3)+ `config.json` + openpyxl + AI(`config/ai_models.json` 通用 HTTP,chat JSON / images_edits),GUI PySide6 5 Tab(已定)。 -- 生产代码:已建立 `app/` 包 + 根入口 `main.py`;`app/cdp.py` 为已验证 CDP 底座,已区分 `CDP.close()` 断开 WebSocket 与 `close_tab()` 关闭浏览器 target;`app/editor.py` 已封装登录状态检测、标题/封面/采集/更新按钮能力,并在采集结束后只关闭本轮自动新建的商品编辑页 tab、保留用户已有 tab,更新提交成功且设置开启时可关闭本轮自动新建商品页,确认后跳回商品列表页时关闭前等待 2 秒;`click_update()` 已补 Shopee 站点侧确认框处理,页面主「更新」后若出现 `確定您要更新商品嗎?` modal(`.eds-modal__content` / `.eds-modal__box`),只点击弹窗主按钮「更新」并避开「立即優化」,且记录确认后是否跳回商品列表页;`replace_cover()` 已实现更新封面统一先校验本地旧封面备份,再点第一张删除、可见确认框、等待图片管理器稳定和上传 input 恢复,随后先点击上传块模拟人工入口、短暂等待、重新获取 input、注入文件上传新图并等待 Shopee CDN 后拖到第一位的代码路径;已修正 2026-06-30 稳定等待回归:删除第一张前不要求上传 input 可用,只等图片列表稳定;删除后再等上传入口恢复;`重複/重复/duplicate` 上传 toast 会立即判为新封面重复错误;`app/image_paths.py` 已统一新采集/新生成图片路径为 `images///__old/new.jpg`,历史 DB 路径继续按原路径读取;`app/appconfig.py` 已实现 `config.json` 默认值/读写/更新、AI 参数、端口读取、Shopee 更新安全与执行模式默认值,`config/ai_models.json` 模型清单 CRUD/过滤/打码/测试连接与 OpenAI-compatible base URL 自动补 endpoint,以及 `mask_secret()`、`sanitize_for_log()`、`redact_secrets()` 敏感信息脱敏工具;`app/ai.py` 已实现 `gen_title()`/`gen_cover()`/`generate_batch()`,按默认文本/图片模型发起通用 HTTP 调用,支持重试、错误脱敏、图片 URL/base64 解析、resolution resize、jpg_quality 保存、先并发标题再并发封面、逐条 `set_generated`、失败 `mark_failed`、步骤级事件/错误回调与停止取消未开始项;`app/prompts.py` 已实现标题提示词读写、封面模板 CRUD 与变量替换;`app/db.py` 已实现 SQLite schema、连接 PRAGMA、批次/账号/任务与阶段写库函数、T-206 批次软删除标记与默认业务查询过滤、T-404a 本地生成结果/更新状态重置函数,以及 `run_logs/run_log_events` 运行日志函数;`app/diagnostics.py` 已实现 gitignore 本地诊断日志、滚动写入、结构化 payload 和自由文本脱敏;`app/excel.py` 已实现多 Excel 输入列解析、整文件列校验、脏行统计跳过、导入批次与任务入库、别名匹配统计、旧标题/旧封面路径回写原 Excel、更新结果回写原 Excel 与另存副本;`app/config.py` 已实现账号 slug 与 user-data-dir 创建;`app/accounts.py` 已实现账号 CRUD 服务、端口默认分配、启动登录、检测登录、生成快捷方式;`app/chrome.py` 已实现 Chrome 参数拼装、启动、CDP 端口探测、PowerShell `.lnk` 快捷方式生成;`app/gui.py` 已实现 PySide6 `MainWindow`、五 Tab、顶部 Tab 栏防误点样式、① 导入采集的 Excel 导入按钮/导入汇总栏/批次筛选与删除批次软删除入口/QTableView 任务列表/未匹配筛选与略过标记/采集旧标题旧封面 worker/采集前账号就绪预检与④引导/采集完成自动回写/旧数据回写重试按钮与 worker/采集运行日志视图、② AI生成左右布局/标题与封面提示词管理/批次/店铺/商品ID/状态筛选/任务列表/变量预览/开始生成/停止/进度/双击新旧封面预览/AI生成运行日志/重置生成结果与 `GenerateWorker`、③ 更新shopee批次/店铺/商品ID/状态筛选栏/任务列表/重置更新状态/更新安全开关拦截/「预览本轮更新」按钮/开始更新确认弹窗/确认后 `ApplyWorker` 按每批最大更新条数分批执行当前筛选全部可更新记录/账号就绪和端口冲突预检/按账号并行可选/逐条 `set_applied`/运行日志/自动回写结果到 Excel/结束汇总弹窗/手动回写重试按钮、④ 账号管理表格/弹窗/按钮/快捷方式与状态栏、密码明文保存提示、⑤ 设置 AI 模型同列上下布局、居中内容区、三列详情/参数表单、API Key 明文保存提示、`AIModelTestWorker` 后台测试连接、角色/生成参数/路径/端口配置、Shopee 更新安全与多账号并行设置并持久化 `config.json`,保存成功后弹轻量提示框,`dry_run` 仅保留内部兼容字段,普通设置页已隐藏测试商品 ID 入口;③ 普通正式更新不再用 `test_item_id` 阻断非测试商品,确认弹窗不再显示测试商品 ID;`app/workers.py` 已实现 `BaseWorker`、通用 signals、取消标记和 `QThread` 启动包装。 -- 测试:`tests/` 已建立;T-006 后纯逻辑改动必须运行 `python -m unittest discover -s tests`,当前覆盖 appconfig/db/config/accounts/chrome 启动与快捷方式/editor 登录检测与商品 tab 生命周期、更新成功后关闭本轮新开 tab、更新封面备份缺失阻断/8张与9张先删第一张/删除确认/上传入口初始不可用/重复图片 toast/物流错误不阻断上传/删图后稳定等待/上传状态诊断/拖首位 mock 路径/Shopee 更新确认框、成功跳回商品列表和残留错误 toast 不覆盖成功 mock 路径/excel 导入/旧字段与更新结果回写/ai 标题与封面 HTTP 解析/`generate_batch` 正常、失败与停止/prompts 读写与渲染/gui ① 导入采集、删除批次软删除与采集诊断日志/gui ② AI生成布局与批次/店铺/商品ID/状态筛选、提示词管理、生成 worker、图片失败诊断日志、双击预览与重置生成结果/gui ③ 更新shopee批次/店铺/商品ID/状态筛选列表、确认弹窗、重置更新状态、Shopee 更新安全拦截、`ApplyWorker` 串行/预览/分批/按账号并行/端口冲突预检、运行日志、结果回写与汇总/gui ④ 账号管理、密码打码与明文保存提示/gui ⑤ AI 模型管理、API Key 打码与明文保存提示、角色/生成参数设置、Shopee 更新安全设置、预览按钮/每批最大更新条数/多账号并行设置、测试商品 ID 限制移除/worker signal 与线程包装;2026-07-01 已完成 5 个真实商品的 T-404 更新验收,后续 CDP/Shopee 改动仍需测试商品手动验证。 +- 生产代码:已建立 `app/` 包 + 根入口 `main.py`;`app/cdp.py` 为已验证 CDP 底座,已区分 `CDP.close()` 断开 WebSocket 与 `close_tab()` 关闭浏览器 target;`app/editor.py` 已封装登录状态检测、标题/封面/采集/更新按钮能力,并在采集结束后只关闭本轮自动新建的商品编辑页 tab、保留用户已有 tab,更新提交成功且设置开启时可关闭本轮自动新建商品页,确认后跳回商品列表页时关闭前等待 2 秒;`click_update()` 已补 Shopee 站点侧确认框处理,页面主「更新」后若出现 `確定您要更新商品嗎?` modal(`.eds-modal__content` / `.eds-modal__box`),只点击弹窗主按钮「更新」并避开「立即優化」,且记录确认后是否跳回商品列表页;`replace_cover()` 已实现更新封面统一先校验本地旧封面备份,再点第一张删除、可见确认框、等待图片管理器稳定和上传 input 恢复,随后先点击上传块模拟人工入口、短暂等待、重新获取 input、注入文件上传新图并等待 Shopee CDN 后拖到第一位的代码路径;已修正 2026-06-30 稳定等待回归:删除第一张前不要求上传 input 可用,只等图片列表稳定;删除后再等上传入口恢复;`重複/重复/duplicate` 上传 toast 会立即判为新封面重复错误;`app/image_paths.py` 已统一新采集/新生成图片路径为 `images///__old/new.jpg`,历史 DB 路径继续按原路径读取;`app/appconfig.py` 已实现 `config.json` 默认值/读写/更新、AI 参数、端口读取、Shopee 更新安全与执行模式默认值,`config/ai_models.json` 模型清单 CRUD/过滤/打码/测试连接与 OpenAI-compatible base URL 自动补 endpoint,以及 `mask_secret()`、`sanitize_for_log()`、`redact_secrets()` 敏感信息脱敏工具;`app/ai.py` 已实现 `gen_title()`/`gen_cover()`/`generate_batch()`,按默认文本/图片模型发起通用 HTTP 调用,支持重试、错误脱敏、图片 URL/base64 解析、resolution resize、jpg_quality 保存、先并发标题再并发封面、逐条 `set_generated`、失败 `mark_failed`、步骤级事件/错误回调与停止取消未开始项;`app/prompts.py` 已实现标题提示词读写、封面模板 CRUD 与变量替换;`app/db.py` 已实现 SQLite schema、连接 PRAGMA、批次/账号/任务与阶段写库函数、T-206 批次软删除标记与默认业务查询过滤、T-404a 本地生成结果/更新状态重置函数、T-509 `update_generated_title()` 本地新标题微调函数,以及 `run_logs/run_log_events` 运行日志函数;`app/diagnostics.py` 已实现 gitignore 本地诊断日志、滚动写入、结构化 payload 和自由文本脱敏;`app/excel.py` 已实现多 Excel 输入列解析、整文件列校验、脏行统计跳过、导入批次与任务入库、别名匹配统计、旧标题/旧封面路径回写原 Excel、更新结果回写原 Excel 与另存副本;`app/config.py` 已实现账号 slug 与 user-data-dir 创建;`app/accounts.py` 已实现账号 CRUD 服务、端口默认分配、启动登录、检测登录、生成快捷方式;`app/chrome.py` 已实现 Chrome 参数拼装、启动、CDP 端口探测、PowerShell `.lnk` 快捷方式生成;`app/gui.py` 已实现 PySide6 `MainWindow`、五 Tab、顶部 Tab 栏防误点样式、① 导入采集的 Excel 导入按钮/导入汇总栏/批次筛选与删除批次软删除入口/QTableView 任务列表/未匹配筛选与略过标记/采集旧标题旧封面 worker/采集前账号就绪预检与④引导/采集完成自动回写/旧数据回写重试按钮与 worker/采集运行日志视图、② AI生成左右布局/标题与封面提示词管理/批次/店铺/商品ID/状态筛选/任务列表/新标题列本地微调/变量预览/开始生成/停止/进度/双击新旧封面预览/AI生成运行日志/重置生成结果与 `GenerateWorker`、③ 更新shopee批次/店铺/商品ID/状态筛选栏/任务列表/开始更新主按钮/重置更新状态右键菜单/更新安全开关拦截与「前往设置」跳转/「检查本轮更新」按钮/开始更新确认弹窗/确认后 `ApplyWorker` 按每批最大更新条数分批执行当前筛选全部可更新记录/账号就绪和端口冲突预检/按账号并行可选/逐条 `set_applied`/运行日志/自动回写结果到 Excel/结束汇总弹窗/手动回写重试按钮、④ 账号管理表格/弹窗/按钮/快捷方式与状态栏、密码明文保存提示、⑤ 设置 AI 模型同列上下布局、居中内容区、三列详情/参数表单、API Key 明文保存提示、`AIModelTestWorker` 后台测试连接、角色/生成参数/路径/端口配置、Shopee 更新安全与多账号并行设置并持久化 `config.json`,保存成功后弹轻量提示框,`dry_run` 仅保留内部兼容字段,普通设置页已隐藏测试商品 ID 入口;③ 普通正式更新不再用 `test_item_id` 阻断非测试商品,确认弹窗不再显示测试商品 ID;`app/workers.py` 已实现 `BaseWorker`、通用 signals、取消标记和 `QThread` 启动包装。 +- 测试:`tests/` 已建立;T-006 后纯逻辑改动必须运行 `python -m unittest discover -s tests`,当前覆盖 appconfig/db/config/accounts/chrome 启动与快捷方式/editor 登录检测与商品 tab 生命周期、更新成功后关闭本轮新开 tab、更新封面备份缺失阻断/8张与9张先删第一张/删除确认/上传入口初始不可用/重复图片 toast/物流错误不阻断上传/删图后稳定等待/上传状态诊断/拖首位 mock 路径/Shopee 更新确认框、成功跳回商品列表和残留错误 toast 不覆盖成功 mock 路径/excel 导入/旧字段与更新结果回写/ai 标题与封面 HTTP 解析/`generate_batch` 正常、失败与停止/prompts 读写与渲染/gui ① 导入采集、删除批次软删除与采集诊断日志/gui ② AI生成布局与批次/店铺/商品ID/状态筛选、提示词管理、生成 worker、图片失败诊断日志、双击预览、重置生成结果与新标题本地编辑/gui ③ 更新shopee批次/店铺/商品ID/状态筛选列表、确认弹窗、开始更新主按钮、重置更新状态右键菜单、Shopee 更新安全拦截与前往设置、`ApplyWorker` 串行/检查/分批/按账号并行/端口冲突预检、运行日志、结果回写与汇总/gui ④ 账号管理、密码打码与明文保存提示/gui ⑤ AI 模型管理、API Key 打码与明文保存提示、角色/生成参数设置、Shopee 更新安全设置、检查按钮/每批最大更新条数/多账号并行设置、测试商品 ID 限制移除/worker signal 与线程包装;2026-07-01 已完成 5 个真实商品的 T-404 更新验收,后续 CDP/Shopee 改动仍需测试商品手动验证。 - 数据:`config.json`、`config/ai_models.json`、`cmshopee.db`、`cmshopee.db-*`、`chrome_user_data_dir/`、`images/`、`logs/` 已由 `.gitignore` 排除;密码与 API Key 本地明文保存但保存/变更时提示,UI 打码,日志/导出必须脱敏;运营填写后的 Excel 业务文件默认忽略,标准空模板 `shopee待处理任务模板.xlsx` 可提交;`app/appconfig.py` 首次读取缺失的 `config.json` 时会在本地写默认配置,`app/db.py` 调用 `init_db()` 时会在本地创建 SQLite DB。 ## 既定设计要点(文档已定) @@ -19,7 +19,7 @@ - 存储:`config.json`(应用设置)+ `config/ai_models.json`(AI 模型清单与本地明文 Key)+ SQLite `cmshopee.db`(账号/任务/各阶段结果,密码本地明文仅参考)+ openpyxl(Excel)+ 本地 `images/`(旧/新封面);密码/API Key 保存或变更时提示,展示和日志/导出必须脱敏。 - 多账号隔离:每账号独立 user-data-dir(非 profile)。 - 账号↔任务绑定:以 Excel“别名”列为权威;未匹配略过,结束弹窗汇总。 -- 执行:默认多账号串行、单条失败继续;③ 提供「预览本轮更新」按钮,预览只写运行日志和汇总,不打开 Shopee、不提交线上、不改任务状态;⑤ 可开启多账号并行(不同账号并行、同账号内串行)并设置每批最大更新条数。③ 点击「开始更新」后先按真实更新安全设置弹窗确认当前筛选范围、任务数量、每批大小和预计批次;确认后做本轮账号 Chrome/CDP/登录态预检,未启动或未登录则弹窗列出账号并中止、不自动打开 Chrome;账号就绪后按批逐条点「更新」提交线上,点击停止后不再开始下一条或下一批。 +- 执行:默认多账号串行、单条失败继续;③ 提供「检查本轮更新」按钮,检查只写运行日志和汇总,不打开 Shopee、不提交线上、不改任务状态;⑤ 可开启多账号并行(不同账号并行、同账号内串行)并设置每批最大更新条数。③ 点击「开始更新」后先按真实更新安全设置弹窗确认当前筛选范围、任务数量、每批大小和预计批次;确认后做本轮账号 Chrome/CDP/登录态预检,未启动或未登录则弹窗列出账号并中止、不自动打开 Chrome;账号就绪后按批逐条点「更新」提交线上,点击停止后不再开始下一条或下一批。 - AI:服务商/模型/Key 由 `config/ai_models.json` 配置;`app/ai.py` 支持 chat JSON 与 images_edits multipart;生成内容直接用于更新,本地留档+回写 Excel 供追溯。 - 登录:人工登录 + 程序检测,不自动登录;④「启动登录」只是打开对应账号浏览器供人工登录,④「检测登录」和①/③预检只验证登录态;无 Shopee tab 时检测入口为 `https:///`(默认 `https://seller.shopee.tw/`);首次未配账号、对应账号 Chrome 未启动或未登录时,① ③ 应禁用或执行前预检提示,并引导去④,③ 不静默启动缺失账号 Chrome。 @@ -33,7 +33,7 @@ | `chrome-remote-debug-lan.md` | 已有 | WSL→Windows CDP 转发排查记录 | | `docs/troubleshooting.md` | 已有 | 常见问题排查;已记录 `config/ai_models.json` 缺失 `category` 导致 GUI 启动报 “AI 模型 category 必须是 text 或 image” 的原因、修复和验证步骤 | | `app/__init__.py` / `app/__main__.py` / `main.py` | 已有 | 正式包与启动入口;`python main.py` / `python -m app` 可运行占位入口 | -| `app/gui.py` | 已有 | T-104/T-105/T-106/T-202/T-202b/T-203/T-204/T-204b/T-205/T-206/T-207/T-302/T-302p/T-303/T-303b/T-401/T-402/T-403/T-404a/T-501/T-501b/T-501c/T-503/T-504/T-506/T-507 产出:PySide6 `QMainWindow` + 五 Tab;顶部 Tab 栏防误点样式;① 导入采集导入按钮、导入汇总栏、批次筛选、删除批次软删除按钮、`QTableView` 任务列表、未匹配筛选与略过标记、采集旧标题旧封面 worker(旧封面路径按批次/店铺/任务细分)、采集前账号就绪预检与④引导、采集完成自动回写与手动重试、采集运行日志视图;② AI生成左右布局、提示词管理、批次/店铺/商品ID/状态筛选栏、任务列表、开始生成/停止/进度、双击新旧封面预览、AI生成运行日志、重置生成结果与 `GenerateWorker`;③ 更新shopee批次/店铺/商品ID/状态筛选栏、任务列表、重置更新状态、Shopee 更新安全拦截、「预览本轮更新」按钮、开始更新确认弹窗、`ApplyWorker` 预览/分批串行/按账号并行、账号与端口预检、运行日志、逐条 `set_applied`、自动回写结果到 Excel、结束汇总弹窗与手动回写重试;④ 账号管理表格、账号弹窗、密码本地明文保存提示、启动登录、检测登录、快捷方式;⑤ 设置 AI 模型下拉、新增/删除、详情编辑、密钥打码与本地明文保存提示、测试连接 worker、默认角色下拉、生成参数、路径端口配置、三列 Shopee 更新安全与执行模式设置,dry_run 用户入口改为③预览按钮,测试商品 ID 普通入口已隐藏且不再阻断正式更新 | +| `app/gui.py` | 已有 | T-104/T-105/T-106/T-202/T-202b/T-203/T-204/T-204b/T-205/T-206/T-207/T-302/T-302p/T-303/T-303b/T-401/T-402/T-403/T-404a/T-501/T-501b/T-501c/T-503/T-504/T-506/T-507/T-508/T-509/T-510 产出:PySide6 `QMainWindow` + 五 Tab;顶部 Tab 栏防误点样式;① 导入采集导入按钮、导入汇总栏、批次筛选、删除批次软删除按钮、`QTableView` 任务列表、未匹配筛选与略过标记、采集旧标题旧封面 worker(旧封面路径按批次/店铺/任务细分)、采集前账号就绪预检与④引导、采集完成自动回写与手动重试、采集运行日志视图;② AI生成左右布局、提示词管理、批次/店铺/商品ID/状态筛选栏、任务列表、新标题列本地编辑、开始生成/停止/进度、双击新旧封面预览、AI生成运行日志、重置生成结果与 `GenerateWorker`;③ 更新shopee批次/店铺/商品ID/状态筛选栏、任务列表、开始更新主按钮、重置更新状态右键菜单、Shopee 更新安全拦截与「前往设置」跳转、「检查本轮更新」按钮、开始更新确认弹窗、`ApplyWorker` 检查/分批串行/按账号并行、账号与端口预检、运行日志、逐条 `set_applied`、自动回写结果到 Excel、结束汇总弹窗与手动回写重试;④ 账号管理表格、账号弹窗、密码本地明文保存提示、启动登录、检测登录、快捷方式;⑤ 设置 AI 模型下拉、新增/删除、详情编辑、密钥打码与本地明文保存提示、测试连接 worker、默认角色下拉、生成参数、路径端口配置、三列 Shopee 更新安全与执行模式设置,dry_run 用户入口改为③检查按钮,测试商品 ID 普通入口已隐藏且不再阻断正式更新 | | `app/workers.py` | 已有 | T-104b 产出:`BaseWorker` + 通用 signals + 取消标记 + `run_worker()` QThread 包装 | | `app/accounts.py` | 已有 | T-105/T-106 产出:账号 CRUD 服务、目录创建、端口分配、启动登录、检测登录、快捷方式 | | `app/editor.py` | 已有 | T-001/T-103/T-205b/T-207/T-501c/T-502 + T-404 补丁产出:登录状态检测、打开商品页、读/写标题、读/下载封面、采集步骤回调、采集后关闭自动新建商品页 tab、上传前等待图片管理器稳定、点击上传块后注入文件并检测 CDN 后拖封面、更新封面统一先校验旧封面备份再删线上第一张、重复图片 toast 明确失败、页面主更新按钮、Shopee 站点侧确认框主按钮、apply_task;更新成功后可按设置关闭本轮自动新建商品页 | @@ -41,11 +41,11 @@ | `app/diagnostics.py` | 已有 | T-207 + AI生成诊断补丁产出:本地 `logs/cmshopee.log` 诊断日志、大小滚动、异常类型/traceback/step/耗时记录,结构化 payload 和自由文本脱敏后写入 | | `app/ai.py` | 已有 | T-301/T-303 + AI生成诊断补丁产出:`gen_title()`/`gen_cover()`/`generate_batch()`;读取默认模型;通用 HTTP 调用;失败重试;错误脱敏;标题/封面步骤级回调;封面按 resolution/jpg_quality 保存并按批次/店铺/任务路径落盘;批量生成先标题后封面、进度回调、逐条落库、失败标记、事件/错误回调、停止取消未开始项 | | `app/prompts.py` | 已有 | T-302p 产出:标题提示词读写、封面模板列表/读取/保存/重命名/删除、变量替换 | -| `app/db.py` | 已有 | T-003/T-206/T-404a/T-504 产出:batches/accounts/tasks schema;batches 软删除字段与默认业务查询过滤;run_logs/run_log_events;WAL/busy_timeout/foreign_keys;账号/批次/任务与 set_* 阶段写库;生成结果/更新状态本地重置;运行日志写入与查询 | +| `app/db.py` | 已有 | T-003/T-206/T-404a/T-504/T-509 产出:batches/accounts/tasks schema;batches 软删除字段与默认业务查询过滤;run_logs/run_log_events;WAL/busy_timeout/foreign_keys;账号/批次/任务与 set_* 阶段写库;生成结果/更新状态本地重置;`update_generated_title()` 本地新标题微调;运行日志写入与查询 | | `app/config.py` | 已有 | T-101 产出:别名→稳定 slug;创建并返回绝对 user-data-dir | | `app/image_paths.py` | 已有 | 本地图片路径 helper:新采集旧封面和新生成封面统一写入 `images///__old/new.jpg`;历史 DB 已存路径继续按原路径读取 | | `app/chrome.py` | 已有 | T-102/T-106 产出:Chrome 启动参数、`subprocess.Popen` 启动、`/json/version` 端口探测、PowerShell `.lnk` 快捷方式 | -| `tests/` | 已有 | T-006/T-201/T-202/T-202b/T-203/T-204/T-204b/T-205/T-206/T-207/T-301/T-302/T-302p/T-303/T-303b/T-401/T-402/T-403/T-501/T-501b/T-501c/T-502/T-503/T-504/T-506/T-507 产出:stdlib unittest 基座;覆盖 appconfig/db/config/accounts/chrome/editor/excel/ai/image_paths/prompts/gui/workers | +| `tests/` | 已有 | T-006/T-201/T-202/T-202b/T-203/T-204/T-204b/T-205/T-206/T-207/T-301/T-302/T-302p/T-303/T-303b/T-401/T-402/T-403/T-501/T-501b/T-501c/T-502/T-503/T-504/T-506/T-507/T-508/T-509/T-510 产出:stdlib unittest 基座;覆盖 appconfig/db/config/accounts/chrome/editor/excel/ai/image_paths/prompts/gui/workers | | `app/excel.py` | 已有 | T-201/T-204/T-403 产出:多文件 Excel 输入列解析、必需列整文件拒绝、脏行逐行跳过、批次/任务入库、匹配统计;按源文件/工作表/行号回写旧标题与旧封面路径;按源文件/工作表/行号回写新标题、新封面路径、更新状态;支持原文件被占用时另存副本 | | `shopee待处理任务模板.xlsx` | 已有,已提交 | 标准空 Excel 模板;单工作表 `待处理任务`,表头 `账号名 | 别名 | 商品id | 旧标题 | 旧封面图片路径 | 新标题 | 新封面图片路径 | 更新状态`;运营复制后填写,填写副本不提交 | | `config.json` / `config/ai_models.json` / `cmshopee.db` / `cmshopee.db-*` / `chrome_user_data_dir/` / `images/` / `logs/` | 本地存在或按需生成,已忽略 | 含配置、密钥、业务、登录态、图片、本地诊断日志,不提交版本库;密码/API Key 保存或变更时提示,展示/日志/导出脱敏 | @@ -63,7 +63,7 @@ 任务状态以 [`06-tasks.md`](06-tasks.md) 为准,历史记录见 [`../progress.md`](../progress.md)。 -- 已完成:T-000(正式代码包结构)、T-001(`app/editor.py` 模块化)、T-002(`app/appconfig.py` + `config.json`)、T-003(SQLite 建表)、T-004(本地数据 gitignore)、T-005(AI 模型清单后端)、T-006(单元测试基座)、T-101(账号 slug/user-data-dir)、T-102(Chrome 启动器)、T-103(登录保活与检测)、T-104(PySide6 五 Tab 主窗口骨架)、T-104b(PySide6 worker 基座)、T-105(Tab④ 账号管理)、T-106(账号快捷方式)、T-201(Excel 导入:解析多文件输入列入库)、T-202(Tab① 任务列表 + 导入按钮 + 别名匹配标记)、T-202b(Tab① 导入汇总栏)、T-203(采集旧标题+旧封面)、T-204(回写旧字段到原 Excel)、T-204b(采集完成自动回写旧字段)、T-205(首次未配账号 / Chrome 未启动 / 未登录引导保护)、T-205b(采集后关闭自动新建商品页 tab)、T-206(Tab① 删除指定批次软删除)、T-301(AI 生成接口)、T-302(Tab② 左右布局与任务列表)、T-302p(提示词管理)、T-303(Tab② 开始生成 + 停止 + 进度)、T-303b(②/③ 商品ID筛选)、T-401(Tab③ 更新列表筛选 + 开始更新确认弹窗)、T-402(Tab③ 确认后串行更新)、T-403(Tab③ 结果回写与结束汇总)、T-501(Tab⑤ AI 模型管理 UI)、T-501b(Tab⑤ 角色与生成参数)、T-501c(Tab⑤ Shopee 更新安全开关)、T-502(换封面删第一张再上传)、T-503(敏感信息本地明文保存提示与日志脱敏)、T-504(多账号并行 / dry-run / 运行日志)、T-207(①采集诊断日志)、② AI生成图片失败诊断日志补丁、T-404a(②/③ 选中记录重置)、T-404(真实 Shopee 单条更新冒烟验收)、T-506(正式使用批量更新体验)、T-507(正式批量更新:移除普通流程测试商品 ID 限制)。 +- 已完成:T-000(正式代码包结构)、T-001(`app/editor.py` 模块化)、T-002(`app/appconfig.py` + `config.json`)、T-003(SQLite 建表)、T-004(本地数据 gitignore)、T-005(AI 模型清单后端)、T-006(单元测试基座)、T-101(账号 slug/user-data-dir)、T-102(Chrome 启动器)、T-103(登录保活与检测)、T-104(PySide6 五 Tab 主窗口骨架)、T-104b(PySide6 worker 基座)、T-105(Tab④ 账号管理)、T-106(账号快捷方式)、T-201(Excel 导入:解析多文件输入列入库)、T-202(Tab① 任务列表 + 导入按钮 + 别名匹配标记)、T-202b(Tab① 导入汇总栏)、T-203(采集旧标题+旧封面)、T-204(回写旧字段到原 Excel)、T-204b(采集完成自动回写旧字段)、T-205(首次未配账号 / Chrome 未启动 / 未登录引导保护)、T-205b(采集后关闭自动新建商品页 tab)、T-206(Tab① 删除指定批次软删除)、T-301(AI 生成接口)、T-302(Tab② 左右布局与任务列表)、T-302p(提示词管理)、T-303(Tab② 开始生成 + 停止 + 进度)、T-303b(②/③ 商品ID筛选)、T-401(Tab③ 更新列表筛选 + 开始更新确认弹窗)、T-402(Tab③ 确认后串行更新)、T-403(Tab③ 结果回写与结束汇总)、T-501(Tab⑤ AI 模型管理 UI)、T-501b(Tab⑤ 角色与生成参数)、T-501c(Tab⑤ Shopee 更新安全开关)、T-502(换封面删第一张再上传)、T-503(敏感信息本地明文保存提示与日志脱敏)、T-504(多账号并行 / dry-run / 运行日志)、T-207(①采集诊断日志)、② AI生成图片失败诊断日志补丁、T-404a(②/③ 选中记录重置)、T-404(真实 Shopee 单条更新冒烟验收)、T-506(正式使用批量更新体验)、T-507(正式批量更新:移除普通流程测试商品 ID 限制)、T-508(③ 更新shopee生产化操作区)、T-509(② 新标题人工微调)、T-510(③ 检查本轮更新文案统一)。 - 下一个可领取任务:**T-505(全流程诊断日志扩展)**。T-206 已完成软删除;T-505 依赖的 T-207/T-504 均已完成。 ## 当前已知限制 @@ -72,12 +72,13 @@ - ① 采集和③真实更新都依赖对应账号 Chrome 已用专属 user-data-dir 和 CDP 端口启动并登录;T-205/T-402 已在执行前拦截未配置账号、Chrome 未启动、CDP 端口不可访问、未登录,并引导去④账号管理。③ 点击「开始更新」后若本轮需要账号未就绪,必须中止本轮更新,不自动打开账号 Chrome、不提交任何商品。 - T-501/T-501b/T-501c 已完成 `config/ai_models.json` 模型清单 UI,以及 `config.json` 里的标题/图片默认模型角色、并发、重试、分辨率、jpg 质量、路径/端口设置和 Shopee 更新安全开关。 -- T-506 已完成:③ 已将用户可见 `dry-run` 改为「预览本轮更新」按钮;真实更新对当前筛选结果按每批最大更新条数自动分批,确认弹窗显示任务数和预计批次,停止为当前商品安全结束后不再开始新任务;⑤ 设置页已改为居中内容区,左右留白已缩短为 T-506 初始实现约 40%,模型详情/角色与生成参数/路径与端口/Shopee 更新安全使用三列布局,长字段跨列;「多账号并行更新」与「最大并行账号数」已合并为同一个横向组件,最大并行账号数紧跟其后且不换行。 +- T-506 已完成:③ 已将用户可见 `dry-run` 改为「检查本轮更新」按钮;真实更新对当前筛选结果按每批最大更新条数自动分批,确认弹窗显示任务数和预计批次,停止为当前商品安全结束后不再开始新任务;⑤ 设置页已改为居中内容区,左右留白已缩短为 T-506 初始实现约 40%,模型详情/角色与生成参数/路径与端口/Shopee 更新安全使用三列布局,长字段跨列;「多账号并行更新」与「最大并行账号数」已合并为同一个横向组件,最大并行账号数紧跟其后且不换行。 - T-507 已完成:普通正式更新移除 `test_item_id` 商品 ID 限制;③ 确认弹窗不再显示测试商品 ID;⑤ 普通设置页隐藏测试商品 ID,只作为历史/调试兼容字段保留;仍保留允许真实提交、允许更新封面、每批最大条数、二次确认、账号就绪预检、多账号并行上限、运行日志和 Excel 回写。 +- T-508/T-509/T-510 已完成:③「开始更新」作为主操作视觉强化,「重置更新状态」移到任务表右键菜单,更新安全拦截弹窗写明具体设置并可跳到⑤;②「新标题」列允许已生成、未提交线上、非运行中任务本地微调,写回 `tasks.new_title`,清空 `last_error` 并回到可更新状态,不触碰 Shopee/CDP/Excel/封面;③ 用户可见「预览本轮更新」已统一改名为「检查本轮更新」,内部 `dry_run` 字段保留。 - T-301/T-303 已完成通用 HTTP AI 接口、批量生成编排和 GUI 接入 mock 单测;真实 AI 生成还需要在 `config/ai_models.json` 填入可用 url/model/api_key 后做一次成本可控的小样本实测;URL 可填完整 endpoint 或 OpenAI-compatible base URL。 - 本地 `config/ai_models.json` 若由旧版本或手工维护,可能缺少 `category`;启动报 “AI 模型 category 必须是 text 或 image” 时,按 [`troubleshooting.md`](troubleshooting.md) 只补 `category` / `enabled` 等非密钥字段,保留 API Key,且不要提交该文件。 - T-403/T-501c 已完成③更新结果回写、结束汇总与真实更新安全开关;2026-06-29 已用测试商品跑到真实编辑页并完成标题/封面替换、页面主「更新」点击,但被 Shopee 站点侧确认框拦住。当前代码已补确认框处理、确认后列表页跳转观测、上传状态诊断、删除后稳定等待和 mock 单测;2026-06-30 用户复跑发现稳定等待顺序回归:满 9 张时删除前要求上传 input 可用,导致不删除第一张图;当前已修正为删除前只等图片列表稳定、删除后再等上传入口恢复,并补 mock 回归测试;2026-06-30 又发现物流/备货页面级校验 toast 会被误判成封面上传失败,当前已修复为上传阶段只识别图片/文件/上传相关错误,物流错误留到提交阶段处理。2026-06-30 用户复跑确认浏览器已成功更新标题/图片并跳转商品列表,但 GUI 仍报 `POST_UPDATE_ERROR`;已修正提交后观测优先级:列表页跳转优先判成功,残留 error toast 不再覆盖成功跳转,错误 toast 仅在未跳转且持续存在时判失败。当前又发现商品 26887160467 手动上传 220KB 新图成功,但代码直接注入文件后上传组件长期转圈;T-404 当前补丁已让 `replace_cover()` 上传前先点击上传块模拟人工入口、短暂等待、重新获取 input 后再 `DOM.setFileInputFiles`,并补 mock 回归测试。25120403046 只有 8 张商品图时未删除第一张导致 Shopee 提示 `有1張重複的圖片`;当前已落地为更新封面统一先删当前第一张再上传,并把 `重複/重复/duplicate` toast 识别为封面上传失败。新采集/新生成图片路径已改为按批次细分,避免同一账号多批次图片混放;历史 DB 已存路径继续可用。确认后若返回我的商品列表页且设置了成功后关闭本次新开编辑页,关闭前等待 2 秒。2026-07-01 用户已用包含 5 个商品 ID 的 Excel 完成导入、AI 生成标题/图片、Tab③ 更新到 Shopee 的真实链路验收,T-404 判定完成。 -- T-404a 已完成:②「重置生成结果」清空本地 AI 结果并退回 collected,默认不删除本地新封面文件;③「重置更新状态」保留新标题/新封面并退回 generated/pending;两者只作用当前选中单条、运行中禁用、写 `run_type=reset` 运行日志。③ 对 `committed=1` 会提示线上已提交过、本地重置不回滚 Shopee、重复更新会再次提交。 +- T-404a 已完成:②「重置生成结果」清空本地 AI 结果并退回 collected,默认不删除本地新封面文件;③「重置更新状态」保留新标题/新封面并退回 generated/pending,当前入口已按 T-508 移到任务表右键菜单;两者只作用当前选中单条、运行中禁用、写 `run_type=reset` 运行日志。③ 对 `committed=1` 会提示线上已提交过、本地重置不回滚 Shopee、重复更新会再次提交。 - T-502 换封面删除流程已完成代码路径、mock 单测和真实 9 图商品不提交流程实测;更新封面统一先删当前第一张,删除前必须已有本地旧封面备份(`old_cover_path` 存在且文件存在),缺失备份时拒绝删除线上图片;mock 已覆盖 8 张图也先删第一张、重复图片 toast 立即失败。本轮实测只操作编辑页并关闭测试 tab,未点击「更新」保存线上。`images/` 新路径按 `batch_id/slug/task_id_item_id` 细分,旧 DB 路径兼容不迁移。 ## 当前可运行内容 diff --git a/docs/routes.md b/docs/routes.md index 814977a..53c9856 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -11,7 +11,7 @@ | Tab | 职责 | 风险 | | --- | --- | --- | | ① 导入采集 | 导入多个 Excel;任务列表;**采集**商品当前的旧标题/旧封面(只读),封面图下载本地;回写 Excel 旧字段 | 只读,低 | -| ② AI生成 | 左侧标题/封面**提示词**;右侧按批次/店铺/商品ID/状态筛选任务列表;AI 生成新标题/新封面;双击看新旧封面 | 不触线上,中 | +| ② AI生成 | 左侧标题/封面**提示词**;右侧按批次/店铺/商品ID/状态筛选任务列表;AI 生成新标题/新封面;已生成任务可本地微调新标题;双击看新旧封面 | 不触线上,中 | | ③ 更新shopee | 对**已生成**任务点击「开始更新」后弹窗确认;确认后打开编辑页换标题+封面并逐条点「更新」提交;结果回写 Excel | **写线上,高** | | ④ 账号管理 | Shopee 账号(账号名/别名/数据目录/端口/密码本地明文仅参考/登录状态);启动登录、检测登录、生成快捷方式 | 中 | | ⑤ 设置 | AI 模型/API Key、本地图片目录、Chrome 路径、默认端口、Shopee 更新安全开关等 | — | @@ -83,12 +83,12 @@ - **封面提示词**:模板下拉(读 `prompts/cover/*.txt`)+ 图标工具栏(新建/保存/另存为/重命名/删除)+ 多行输入 + 「插入标题」(插 `{新标题}`)/「预览」(变量替换后查看)。 - 变量:`{旧标题}`/`{新标题}`/`{商品id}`/`{店铺}`,生成前按任务替换。 - 右上:按导入批次 / 店铺 / 商品ID / 状态筛选任务;商品ID输入框按包含匹配 `item_id`,清空表示全部。 -- 右下:任务列表(店铺名、商品id、旧标题、新标题、状态)+ AI生成运行日志;**双击某条 → 弹窗展示旧封面 | 新封面**(纯查看)。 +- 右下:任务列表(店铺名、商品id、旧标题、新标题、状态)+ AI生成运行日志;已生成、未提交线上、非运行中的任务可双击「新标题」列本地微调,写回 `tasks.new_title`,清空 `last_error` 并回到可更新;双击其他列弹窗展示旧封面 | 新封面(纯查看)。 - 底部**单个「开始生成」+「停止」**:开始生成 = **先按 `title_concurrency` 并发生成标题,接着按 `image_concurrency` 并发生成封面**;进度实时显示 标题/封面/失败 计数。 - 生成参数(标题/图片并发数、失败重试、分辨率、jpg 质量、模型/Key)在 **⑤ 设置**,不在本 Tab 重复。 - 每条/每张完成即写库;`run_type=generate` 写 `run_logs/run_log_events`,图片生成失败同时写本地 `logs/cmshopee.log` 脱敏 traceback;「停止」取消未开始项,可再次「开始生成」对剩余继续。 - 「重置生成结果」仅作用当前选中单条,运行中禁用;确认后只改本地 DB,清空 `new_title/new_cover_path/last_error` 并退回 `collected/success` 供重新生成,默认不删除本地新封面文件。若该记录已提交过线上,必须在确认框提示本地重置不回滚 Shopee。 -- **无逐条人工审核环节**;新标题直接用 AI 输出(不可编辑);可选对单行 `重生成`。生成完即可进入 ③,③ 开始更新前会做批量确认。 +- **无逐条人工审核环节**;新标题默认直接用 AI 输出,但允许对已生成且未提交线上的单行做本地微调;可选对单行 `重生成`。生成完即可进入 ③,③ 开始更新前会做批量确认。 ## ③ 更新shopee @@ -99,27 +99,28 @@ │ ┌───────────────────────────────────────────────────────────┐ │ │ │ 店铺 商品ID 新标题 新封面 阶段 结果 │ │ │ └───────────────────────────────────────────────────────────┘ │ -│ 运行日志:预览/真实更新逐条留痕 │ -│ [预览本轮更新] [▶ 开始更新] [■停止] [重置更新状态] [回写结果到 Excel] │ +│ 运行日志:检查/真实更新逐条留痕 │ +│ [检查本轮更新] [▶ 开始更新] [■停止] [回写结果到 Excel] │ └───────────────────────────────────────────────────────────────┘ ``` - 顶部**按批次 / 店铺 / 商品ID / 状态筛选**(与 ①②一致);商品ID输入框按包含匹配 `item_id`,清空表示全部;「开始更新」作用于**当前筛选结果**,是一道范围控制。 - 店铺筛选:建议**逐店铺更新**(每店铺需先启动其 Chrome 并登录)。 - 状态筛选:`已生成` 只跑未更新的;`失败` 用于**失败重试**;`已更新成功/略过` 仅查看。 -- 「预览本轮更新」只读取当前筛选结果并写运行日志,不打开 Shopee、不提交、不改任务状态;弹窗/日志展示总任务数、店铺分布、会更新标题/封面、略过原因、每批最大条数和预计批次数。 -- 点击「开始更新」先读取 ⑤ `shopee_update` 安全设置:未允许真实提交,或包含新封面但未允许封面更新时,直接弹警告并阻断;普通正式更新不再以测试商品 ID 限制当前筛选结果,允许当前筛选结果包含多个真实商品 ID;`max_items_per_run` 作为**每批最大更新条数**,当前筛选总数超过该值时不阻断,而是自动分批执行。 +- 「检查本轮更新」只读取当前筛选结果并写运行日志,不打开 Shopee、不提交、不改任务状态;弹窗/日志展示总任务数、店铺分布、会更新标题/封面、略过原因、每批最大条数和预计批次数。 +- 点击「开始更新」先读取 ⑤ `shopee_update` 安全设置:未允许真实提交,或包含新封面但未允许封面更新时,直接弹警告并阻断;拦截弹窗必须写明具体未开启的设置项,并提供「前往设置」跳到 ⑤;普通正式更新不再以测试商品 ID 限制当前筛选结果,允许当前筛选结果包含多个真实商品 ID;`max_items_per_run` 作为**每批最大更新条数**,当前筛选总数超过该值时不阻断,而是自动分批执行。 - 安全开关通过后,弹窗展示本次筛选条件、任务总数、每批最大条数、预计批次数、安全设置和“将提交线上”的风险提示;用户点「是/确认」才开始,点「否/取消」不执行。 - 真实更新第一条商品前做账号就绪预检:按当前筛选结果汇总需要的账号;无账号、Chrome 未启动、CDP 端口不可访问、未登录或端口冲突时,弹窗列出具体账号/原因并中止本轮,不自动调用「启动登录」或静默打开 Chrome。 - 对确认后的**已生成(generated)任务**执行:打开编辑页换标题+换封面 → 点页面「更新」 → 如 Shopee 弹出“確定您要更新商品嗎?”确认框(`.eds-modal__content` / `.eds-modal__box`),则只点弹窗主按钮「更新」提交,不点「立即優化」。 - 更新封面时统一按替换第一张执行:删除第一张前必须已有该任务的本地旧封面备份(①采集得到的 `old_cover_path` 且文件存在);备份缺失时阻断该条更新并提示先采集/修复备份,不盲删线上图片。 - 默认串行、单条失败继续;⑤ 可开启多账号并行,不同账号同时执行,同一账号内仍串行;真实更新前若本轮账号调试端口冲突则阻断。 -- 「预览本轮更新」只写运行日志与预览汇总,不打开 Shopee、不调用 `editor.apply_task()`、不写任务状态、不回写 Excel。 -- 真实更新每条立即写回 SQLite(committed/状态/error),失败不阻塞后续任务;预览和真实更新都会写 `run_logs/run_log_events`。 +- 「检查本轮更新」只写运行日志与检查汇总,不打开 Shopee、不调用 `editor.apply_task()`、不写任务状态、不回写 Excel。 +- 真实更新每条立即写回 SQLite(committed/状态/error),失败不阻塞后续任务;检查和真实更新都会写 `run_logs/run_log_events`。 - 分批更新时「停止」为协作式停止:已开始的当前商品跑到安全边界并写库后停止,不再开始新商品、不进入下一批;未开始任务保持原状态,下次可继续。 - 更新前做账号就绪预检:无账号、对应账号 Chrome 未启动、CDP 端口不可访问或未登录时整体阻断并引导去④账号管理,不进入逐条提交,也不自动打开账号 Chrome。 - 若 ⑤ 开启“成功后关闭本次新开编辑页”,则仅关闭本轮程序自动新开且成功提交的商品页;确认后跳回我的商品列表页时,关闭前等待 2 秒;失败任务和用户原本打开的 tab 不关闭。 -- 「重置更新状态」仅作用当前选中单条,运行中禁用;保留 `new_title/new_cover_path`,只把本地状态退回可更新,用于重复测试上传/提交。若 `committed=1`,确认框必须提示线上已提交过、本地重置不回滚 Shopee、重复更新会再次提交;不得静默清除 committed 历史事实。 +- 「开始更新」是③的主操作按钮,视觉上必须强于其他批处理按钮。 +- 「重置更新状态」从底部批处理按钮移到任务表右键菜单/高级入口,仅作用当前选中单条,运行中禁用;保留 `new_title/new_cover_path`,只把本地状态退回可更新,用于重复测试上传/提交。若 `committed=1`,确认框必须提示线上已提交过、本地重置不回滚 Shopee、重复更新会再次提交;不得静默清除 committed 历史事实。 - 更新完成后自动回写原 Excel:写入新标题、新封面图片路径、更新状态;原文件被锁时提示关闭后点击「回写结果到 Excel」手动重试。 - 自动回写完成后弹窗汇总成功/失败/略过数量与 Excel 回写文件/行数。 @@ -151,7 +152,7 @@ - Shopee 更新安全(T-501c/T-506 已接入):组件组改为 3 个组件一组;允许真实提交、允许更新封面、每批最大更新条数、成功后关闭本次新开编辑页等短字段三列排列;「多账号并行更新」与「最大并行账号数」必须合并为同一个横向组件,最大并行账号数紧跟在多账号并行更新后面,不允许换到下一行;测试商品 ID 仅作为历史/调试兼容字段保留,不参与普通正式更新安全检查,普通设置页已隐藏该入口。 - 默认关闭真实提交和封面更新,每批最大更新条数默认 1。 - ③ 点击「开始更新」会读取这些设置,先拦截不符合条件的更新,再弹确认框。 -- 更新执行模式(T-504/T-506 已接入):⑤ 只保留多账号并行更新、最大并行账号数等执行设置;“dry-run”不再作为用户可见开关,改到③成为「预览本轮更新」按钮。 +- 更新执行模式(T-504/T-506 已接入):⑤ 只保留多账号并行更新、最大并行账号数等执行设置;“dry-run”不再作为用户可见开关,改到③成为「检查本轮更新」按钮。 - 默认多账号并行关闭;开启多账号并行后同一账号内仍串行。 ## 流程导航 @@ -179,12 +180,12 @@ | `MainWindow(QMainWindow)` | 根窗口 | 持有 `QTabWidget`、状态栏、全局消息 | | `CollectTab(QWidget)` | ① | 导入、任务表、采集、回写 | | `GenerateTab(QWidget)` | ② | 左提示词管理 + 右筛选/任务列表;双击看新旧封面;开始生成/停止/进度已接入 `GenerateWorker` | -| `ApplyTab(QWidget)` | ③ | 已生成任务筛选 +「预览本轮更新」+ Shopee 更新安全拦截 + 分批开始更新确认 + 预览/真实更新运行日志 + 结果回写与结束汇总 | +| `ApplyTab(QWidget)` | ③ | 已生成任务筛选 +「检查本轮更新」+ Shopee 更新安全拦截 + 分批开始更新确认 + 检查/真实更新运行日志 + 结果回写与结束汇总 | | `AccountsTab(QWidget)` | ④ | 账号增删改、启动登录、检测登录、生成快捷方式 | | `SettingsTab(QWidget)` | ⑤ | AI 模型 master-detail 管理 + 响应式三列设置表单 + 角色/生成参数/路径/端口配置 + Shopee 更新安全 | | `TaskTableModel(QAbstractTableModel)` | ①②③ | 任务表格数据模型,供 `QTableView` 使用 | | `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()`,失败继续,写运行日志 | | `AIModelTestWorker(BaseWorker)` | ⑤ | 后台调用 `appconfig.test_ai_model()` 测试模型连接 | | `WriteBackWorker(BaseWorker)` | ①③ | ①回写旧字段;③回写新标题/新封面/更新状态 | diff --git a/progress.md b/progress.md index deb2efc..6098e0f 100644 --- a/progress.md +++ b/progress.md @@ -909,4 +909,17 @@ - GUI:① 导入采集页新增批次下拉和「删除批次」按钮;只能删除具体批次,运行中禁用;确认框显示任务数、已提交线上数,并提示不会回滚 Shopee。确认后刷新①自身和②/③批次筛选与任务列表。 - 测试:新增 DB 软删除默认过滤测试;新增 GUI 软删除回归测试,覆盖删除后①/②/③下拉和任务列表都不再包含该批次,且默认 `get_task` 也不能绕过软删除。 - 验证:`python -m py_compile app\db.py app\gui.py tests\test_db.py tests\test_gui.py` 通过;`python -m unittest discover -s tests -p "test_db.py"` 通过(6 tests);`python -m unittest discover -s tests -p "test_gui.py" -k soft_deletes` 通过(1 test);`python -m compileall app main.py` 通过;`python -m unittest discover -s tests` 通过(143 tests);`git diff --check` 无空白错误(仅 LF/CRLF 提示)。 -- 下一步:T-505 全流程诊断日志扩展。 \ No newline at end of file +- 下一步:T-505 全流程诊断日志扩展。 +## 【2026-07-01】T-508/T-509 完成 · 更新区生产化与新标题微调 + +- 产品化:③「开始更新」改为主操作视觉;「重置更新状态」从底部批处理区移到任务表右键菜单,保持运行中禁用,避免与批量线上提交放在同一操作组。 +- 安全拦截:③ 更新安全开关阻断弹窗现在写明具体未开启设置,并提供「前往设置」跳到⑤,便于正式使用时快速修正配置。 +- 新标题微调:②「新标题」列允许已生成、未提交线上、非运行中任务双击本地编辑;写回 `tasks.new_title`,保留 `stage=generated`,清空 `last_error` 并回到可更新状态,不触碰 Shopee/CDP/Excel/封面。 +- 测试:新增 DB 与 GUI 回归测试,覆盖本地标题编辑、③ 右键重置入口、开始更新主按钮标识、安全拦截前往设置。 +- 验证:`python -m compileall app main.py` 通过;`python -m unittest discover -s tests` 通过(145 tests);`git diff --check` 无空白错误,仅 LF/CRLF 提示。 +## 【2026-07-01】T-510 完成 · 「预览本轮更新」改名为「检查本轮更新」 + +- 文档:先同步 `docs/00-ai-start-here.md`、`docs/02-requirements.md`、`docs/04-architecture.md`、`docs/06-tasks.md`、`docs/api.md`、`docs/routes.md`、`docs/current-state.md`,新增 T-510,明确用户可见按钮/弹窗/日志统一使用「检查本轮更新」。 +- 代码:`app/gui.py` 将③按钮、风险提示、确认框标题、取消/开始/完成状态、完成弹窗和 dry-run 运行日志文案从「预览本轮更新」改为「检查本轮更新」;内部 `preview_update_button`、`preview_update()` 与 `dry_run` 字段保留,不改变执行语义。 +- 测试:更新 `tests/test_gui.py` 的按钮文案、状态栏和 dry-run 日志断言。 +- 验证:`python -m py_compile app\gui.py tests\test_gui.py` 通过;`python -m unittest discover -s tests -p "test_gui.py"` 通过(57 tests);`python -m compileall app main.py` 通过;`python -m unittest discover -s tests` 通过(145 tests);`git diff --check` 无空白错误,仅 LF/CRLF 提示。 \ No newline at end of file diff --git a/tests/test_db.py b/tests/test_db.py index d2e4ee3..54b8493 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -100,6 +100,14 @@ class DbTests(TempDirMixin, unittest.TestCase): self.assertEqual("generated", generated.stage) self.assertEqual("新标题", generated.new_title) + db.mark_failed(task.id, "generate", "标题需调整", path=db_path) + db.update_generated_title(task.id, "人工标题", path=db_path) + edited = db.list_tasks(path=db_path)[0] + self.assertEqual("generated", edited.stage) + self.assertEqual("pending", edited.status) + self.assertEqual("人工标题", edited.new_title) + self.assertIsNone(edited.last_error) + db.set_applied(task.id, False, "按钮禁用", path=db_path) apply_failed = db.list_tasks(path=db_path)[0] self.assertEqual("generated", apply_failed.stage) @@ -112,6 +120,8 @@ class DbTests(TempDirMixin, unittest.TestCase): self.assertEqual("success", applied.status) self.assertEqual(1, applied.committed) self.assertEqual(2, applied.apply_attempts) + with self.assertRaises(db.DbError): + db.update_generated_title(task.id, "线上后改标题", path=db_path) self.assert_removed(temp_dir) diff --git a/tests/test_gui.py b/tests/test_gui.py index 19e4d04..b7085b4 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -634,6 +634,46 @@ class GuiTests(TempDirMixin, unittest.TestCase): self.assert_removed(temp_dir) + def test_generate_tab_allows_editing_generated_title_locally(self): + with self.make_temp_dir() as temp_dir: + cfg = self.make_config(temp_dir) + accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg) + batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"]) + db.insert_tasks( + batch_id, + [ + { + "source_file_abs": os.path.join(temp_dir, "input.xlsx"), + "source_sheet": "商品", + "source_row": 2, + "account_name": "Excel主店", + "alias": "alias-a", + "item_id": "51100639510", + } + ], + path=cfg["db_path"], + ) + task = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])[0] + db.set_collected(task.id, "旧标题", "old.jpg", path=cfg["db_path"]) + db.set_generated(task.id, "AI标题", "new.jpg", path=cfg["db_path"]) + db.mark_failed(task.id, "generate", "标题需微调", path=cfg["db_path"]) + statuses = [] + tab = GenerateTab(config=cfg, status_callback=statuses.append) + self.addCleanup(tab.close) + index = tab.model.index(0, 3) + + self.assertTrue(bool(tab.model.flags(index) & gui.Qt.ItemIsEditable)) + self.assertTrue(tab.model.setData(index, "人工微调标题", gui.Qt.EditRole)) + + updated = db.get_task(task.id, path=cfg["db_path"]) + self.assertEqual("generated", updated.stage) + self.assertEqual("pending", updated.status) + self.assertEqual("人工微调标题", updated.new_title) + self.assertIsNone(updated.last_error) + self.assertEqual("人工微调标题", tab.model.index(0, 3).data()) + self.assertIn("已修改新标题", statuses[-1]) + + self.assert_removed(temp_dir) def test_generate_tab_resets_selected_generated_result_and_writes_reset_log(self): with self.make_temp_dir() as temp_dir: cfg = self.make_config(temp_dir) @@ -934,11 +974,14 @@ class GuiTests(TempDirMixin, unittest.TestCase): self.assertIsInstance(tab.task_table, QTableView) self.assertEqual(["店铺", "商品ID", "新标题", "新封面", "阶段", "结果"], tab.model.HEADERS) - self.assertEqual("预览本轮更新", tab.preview_update_button.text()) + self.assertEqual("检查本轮更新", tab.preview_update_button.text()) self.assertEqual("开始更新", tab.start_update_button.text()) self.assertEqual("applyItemFilter", tab.item_filter.objectName()) self.assertEqual("停止", tab.stop_update_button.text()) self.assertEqual("重置更新状态", tab.reset_update_button.text()) + self.assertTrue(tab.reset_update_button.isHidden()) + self.assertEqual(gui.Qt.CustomContextMenu, tab.task_table.contextMenuPolicy()) + self.assertEqual("startUpdateButton", tab.start_update_button.objectName()) self.assertFalse(tab.stop_update_button.isEnabled()) self.assertEqual(2, tab.model.rowCount()) self.assertEqual("主店", tab.model.index(0, 0).data()) @@ -1212,7 +1255,7 @@ class GuiTests(TempDirMixin, unittest.TestCase): self.assertTrue(tab.apply_worker.dry_run) self.assertTrue(tab.apply_worker.parallel_accounts) self.assertEqual(2, tab.apply_worker.max_parallel_accounts) - self.assertEqual("开始预览本轮更新:1 条", statuses[-1]) + self.assertEqual("开始检查本轮更新:1 条", statuses[-1]) self.assert_removed(temp_dir) @@ -1243,13 +1286,14 @@ class GuiTests(TempDirMixin, unittest.TestCase): self.addCleanup(tab.close) tab.item_filter.setText("51100639510") - with mock.patch("app.gui.QMessageBox.warning") as warning, \ + with mock.patch.object(tab, "_show_update_safety_error") as safety_error, \ mock.patch("app.gui.QMessageBox.question") as question, \ mock.patch("app.gui.run_worker") as run_worker: tab.start_update() - message = warning.call_args[0][2] + message = safety_error.call_args[0][0] self.assertIn("允许真实提交线上商品", message) + self.assertIn("⑤设置", message) question.assert_not_called() run_worker.assert_not_called() self.assertIn("已阻止本次更新", statuses[-1]) @@ -1282,12 +1326,14 @@ class GuiTests(TempDirMixin, unittest.TestCase): tab = ApplyTab(config=cfg) self.addCleanup(tab.close) - with mock.patch("app.gui.QMessageBox.warning") as warning, \ + with mock.patch.object(tab, "_show_update_safety_error") as safety_error, \ mock.patch("app.gui.QMessageBox.question") as question, \ mock.patch("app.gui.run_worker") as run_worker: tab.start_update() - self.assertIn("允许更新封面", warning.call_args[0][2]) + message = safety_error.call_args[0][0] + self.assertIn("允许更新封面", message) + self.assertIn("⑤设置", message) question.assert_not_called() run_worker.assert_not_called() @@ -1321,6 +1367,28 @@ class GuiTests(TempDirMixin, unittest.TestCase): self.assert_removed(temp_dir) + def test_apply_tab_safety_error_popup_can_open_settings_tab(self): + with self.make_temp_dir() as temp_dir: + opened = [] + tab = ApplyTab( + config=self.make_config(temp_dir), + open_settings_callback=lambda: opened.append(True), + ) + self.addCleanup(tab.close) + settings_button = object() + box = mock.Mock() + box.addButton.side_effect = [settings_button, object()] + box.clickedButton.return_value = settings_button + + with mock.patch("app.gui.QMessageBox", return_value=box) as message_box: + tab._show_update_safety_error("设置未开启") + + message_box.assert_called_once_with(tab) + box.setWindowTitle.assert_called_once_with("更新安全开关") + box.setText.assert_called_once_with("设置未开启") + self.assertEqual([True], opened) + + self.assert_removed(temp_dir) def test_apply_worker_applies_success_failure_and_unmatched_serially(self): with self.make_temp_dir() as temp_dir: cfg = self.make_config(temp_dir) @@ -1493,7 +1561,7 @@ class GuiTests(TempDirMixin, unittest.TestCase): unchanged = db.list_tasks(batch_id=batch_id, path=cfg["db_path"]) self.assertTrue(all(task.stage == "generated" for task in unchanged)) self.assertTrue(all(task.status == "success" for task in unchanged)) - self.assertTrue(any("预览" in line for line in logs)) + self.assertTrue(any("检查" in line for line in logs)) run_log = db.list_run_logs(run_type="apply", path=cfg["db_path"])[0] self.assertEqual(1, run_log.dry_run) self.assertEqual("done", run_log.status)