diff --git a/app/ai.py b/app/ai.py index df454a7..d1b2187 100644 --- a/app/ai.py +++ b/app/ai.py @@ -180,7 +180,7 @@ def analyze_product_images( cfg = appconfig.load_config() if config is None else config ai_cfg = appconfig.ai_config(cfg) if _ai_backend(ai_cfg) != "cmhub": - raise AIError("商品套图AI帮写需要使用 cmhub 图片理解服务,请到⑤设置配置 cmhub。") + raise AIError("商品套图AI帮写需要使用 cmhub 图片理解服务,请到设置配置 cmhub。") paths = _validate_cmhub_vision_images(image_paths) runtime = _cmhub_runtime(cfg, "vision", cmhub_config_path) @@ -878,9 +878,9 @@ def fetch_cmhub_models(base_url, api_key, connect_timeout=10, read_timeout=30, u base_url = str(base_url or "").strip() api_key = str(api_key or "") if not base_url: - raise CMHubError("cmhub_not_configured", "请去⑤设置配置 cmhub Base URL") + raise CMHubError("cmhub_not_configured", "请去设置配置 cmhub Base URL") if not api_key: - raise CMHubError("cmhub_not_configured", "请去⑤设置配置 cmhub API Key") + raise CMHubError("cmhub_not_configured", "请去设置配置 cmhub API Key") _apply_cmhub_proxy(use_system_proxy) data = _cmhub_call_with_retry( "GET", @@ -904,9 +904,9 @@ def fetch_cmhub_balance(base_url, api_key, connect_timeout=10, read_timeout=30, base_url = str(base_url or "").strip() api_key = str(api_key or "") if not base_url: - raise CMHubError("cmhub_not_configured", "请去⑤设置配置 cmhub Base URL") + raise CMHubError("cmhub_not_configured", "请去设置配置 cmhub Base URL") if not api_key: - raise CMHubError("cmhub_not_configured", "请去⑤设置配置 cmhub API Key") + raise CMHubError("cmhub_not_configured", "请去设置配置 cmhub API Key") _apply_cmhub_proxy(use_system_proxy) data = _cmhub_call_with_retry( "GET", @@ -1455,7 +1455,7 @@ def _cmhub_runtime(config, operation, cmhub_config_path): if missing: raise CMHubError( "cmhub_not_configured", - "请去⑤设置配置 cmhub:缺少 " + "、".join(missing), + "请去设置配置 cmhub:缺少 " + "、".join(missing), retryable=False, ) use_system_proxy = bool(hub.get("use_system_proxy", False)) @@ -1765,7 +1765,7 @@ def _cmhub_retryable(code): def _cmhub_user_message(code, message): defaults = { "insufficient_points": "点数不足,请先充值", - "unauthorized": "cmhub API Key 无效,请去⑤设置重填", + "unauthorized": "cmhub API Key 无效,请去设置重填", "account_disabled": "cmhub 账号已禁用,请去网页端处理", "bad_request": "cmhub 请求参数错误", "model_not_allowed": "cmhub 模型别名无权限", diff --git a/app/cmhub_models.py b/app/cmhub_models.py index 7b7937e..0537cba 100644 --- a/app/cmhub_models.py +++ b/app/cmhub_models.py @@ -279,7 +279,7 @@ def configured_alias_summary(alias, *, model=None): tier = model_tier(model) if alias: return f"cmhub 托管{tier_label(tier)},生图别名 {alias},扣点以返回结果为准" - return "cmhub 托管默认档,未配置生图别名;请先到⑤设置选择生图别名" + return "cmhub 托管默认档,未配置生图别名;请先到设置选择生图别名" def _recommended_for_text(model): diff --git a/app/gui/main_window.py b/app/gui/main_window.py index 6c48439..44d935d 100644 --- a/app/gui/main_window.py +++ b/app/gui/main_window.py @@ -100,7 +100,7 @@ class MainWindow(QMainWindow): self.setWindowTitle(display_name()) _fit_and_center_window(self) self.setStyleSheet(BUTTON_BASE_STYLE) - self._settings_tab_index = TAB_TITLES.index("⑤ 设置") + self._settings_tab_index = TAB_TITLES.index("设置") self._last_tab_index = 0 self._reverting_tab_change = False self.tabs = QTabWidget() @@ -109,7 +109,6 @@ class MainWindow(QMainWindow): self.tabs.currentChanged.connect(self._on_tab_changed) for title in TAB_TITLES: self.tabs.addTab(self._build_tab(title), title) - self.tabs.setTabIcon(TAB_TITLES.index("③ 更新蝦皮"), _warning_dot_icon()) self.setCentralWidget(self.tabs) self.show_status("就绪", level="muted") if startup_status: @@ -153,20 +152,20 @@ class MainWindow(QMainWindow): open_accounts_callback=lambda: self.open_accounts_tab(), open_settings_callback=lambda: self.open_settings_tab(), ) - if title == "④ 账号管理": + if title == "账号管理": return AccountsTab( db_path=self.db_path, config=self.config, status_callback=self.show_status, ) - if title == "⑤ 设置": + if title == "设置": return SettingsTab( config=self.config, config_path=self.config_path, ai_models_path=self.ai_models_path, status_callback=self.show_status, ) - if title == "⑥ 商品套图": + if title == "商品套图": return ProductSuiteTab( db_path=self.db_path, config=self.config, @@ -209,7 +208,7 @@ class MainWindow(QMainWindow): return True box = QMessageBox(self) box.setWindowTitle("未保存更改") - box.setText("⑤ 设置有未保存更改。要先保存再离开吗?") + box.setText("设置有未保存更改。要先保存再离开吗?") save_button = box.addButton("保存", QMessageBox.AcceptRole) discard_button = box.addButton("放弃", QMessageBox.DestructiveRole) box.addButton("取消", QMessageBox.RejectRole) @@ -229,7 +228,7 @@ class MainWindow(QMainWindow): event.ignore() def open_accounts_tab(self): - self.tabs.setCurrentIndex(TAB_TITLES.index("④ 账号管理")) + self.tabs.setCurrentIndex(TAB_TITLES.index("账号管理")) def open_settings_tab(self): - self.tabs.setCurrentIndex(TAB_TITLES.index("⑤ 设置")) + self.tabs.setCurrentIndex(TAB_TITLES.index("设置")) diff --git a/app/gui/tabs/apply.py b/app/gui/tabs/apply.py index b8cfcd6..d4834b2 100644 --- a/app/gui/tabs/apply.py +++ b/app/gui/tabs/apply.py @@ -212,7 +212,7 @@ class ApplyTab(QWidget): self.empty_state_card, self.empty_state_label, self.empty_state_button, - "第一步:前往『④账号管理』配置并登录账号,再回到③更新蝦皮。", + "第一步:前往『账号管理』配置并登录账号,再回到③更新蝦皮。", self.open_accounts_callback is not None, ) return @@ -766,7 +766,7 @@ class ApplyTab(QWidget): full_message = ( f"{message}\n\n" "本轮更新已中止,不会自动打开账号 Chrome,也不会提交任何商品。\n" - "请先到「④ 账号管理」配置账号、启动对应账号 Chrome,并确认已人工登录蝦皮。" + "请先到「账号管理」配置账号、启动对应账号 Chrome,并确认已人工登录蝦皮。" ) QMessageBox.warning(self, "账号未就绪", full_message) self._set_status(full_message.replace("\n", " ")) diff --git a/app/gui/tabs/collect.py b/app/gui/tabs/collect.py index cdc137b..38d8dff 100644 --- a/app/gui/tabs/collect.py +++ b/app/gui/tabs/collect.py @@ -410,7 +410,7 @@ class CollectTab(QWidget): full_message = ( f"{message}\n\n" "本轮采集已中止。\n" - "请先到「④ 账号管理」检查账号配置、Chrome 路径和登录状态。" + "请先到「账号管理」检查账号配置、Chrome 路径和登录状态。" ) QMessageBox.warning(self, "账号未就绪", full_message) self._set_status(full_message.replace("\n", " ")) @@ -928,7 +928,7 @@ class CollectTab(QWidget): if skip_summary: lines.append(skip_summary) if skip_counts[ALIAS_UNMATCHED] > 0: - lines.append("请检查 Excel 别名是否与④账号管理中的账号别名一致。") + lines.append("请检查 Excel 别名是否与账号管理中的账号别名一致。") if launched: lines.append( "本轮已自动启动账号 Chrome:" @@ -940,7 +940,7 @@ class CollectTab(QWidget): + "、".join(self._account_label(item) for item in login_required) ) if skip_counts[LOGIN_REQUIRED] > 0: - lines.append("请到④账号管理完成对应账号登录后,再重新采集略过任务。") + lines.append("请到账号管理完成对应账号登录后,再重新采集略过任务。") if launched or reused or login_required: lines.append("采集结束后不会自动关闭账号 Chrome,请按需自行关闭。") text = "\n".join(lines) @@ -1078,7 +1078,7 @@ class CollectTab(QWidget): self.empty_state_card, self.empty_state_label, self.empty_state_button, - "第一步:前往『④账号管理』配置并登录账号,再回到①导入 Excel。", + "第一步:前往『账号管理』配置并登录账号,再回到①导入 Excel。", self.open_accounts_callback is not None, ) return diff --git a/app/gui/tabs/generate.py b/app/gui/tabs/generate.py index 9fd7a47..cf3b167 100644 --- a/app/gui/tabs/generate.py +++ b/app/gui/tabs/generate.py @@ -2365,7 +2365,7 @@ class GenerateTab(QWidget): self.empty_state_card, self.empty_state_label, self.empty_state_button, - "第一步:前往『④账号管理』配置并登录账号,再回到②生成标题和封面。", + "第一步:前往『账号管理』配置并登录账号,再回到②生成标题和封面。", self.open_accounts_callback is not None, ) return diff --git a/app/gui/tabs/image_studio.py b/app/gui/tabs/image_studio.py index 88ff60a..27394e7 100644 --- a/app/gui/tabs/image_studio.py +++ b/app/gui/tabs/image_studio.py @@ -781,7 +781,7 @@ class ImageStudioTab(QWidget): account.alias, ) if not self.accounts: - self.account_combo.addItem("暂无账号,请先到④账号管理添加", "") + self.account_combo.addItem("暂无账号,请先到账号管理添加", "") def refresh_projects(self): try: diff --git a/app/gui/tabs/product_suite.py b/app/gui/tabs/product_suite.py index 2aabd42..7ef830d 100644 --- a/app/gui/tabs/product_suite.py +++ b/app/gui/tabs/product_suite.py @@ -2472,7 +2472,7 @@ class ProductSuiteTab(QWidget): Qt.ToolTipRole, ) if not self.accounts: - self.account_combo.addItem("暂无账号,请先到④账号管理添加", "") + self.account_combo.addItem("暂无账号,请先到账号管理添加", "") index = self.account_combo.findData(selected) if index >= 0: self.account_combo.setCurrentIndex(index) @@ -2889,7 +2889,7 @@ class ProductSuiteTab(QWidget): return False if self._account_for_alias(state.account_alias) is None: if show_message: - self._message("账号不可用", "所选账号不存在,请到④账号管理刷新账号。") + self._message("账号不可用", "所选账号不存在,请到账号管理刷新账号。") return False return True @@ -2917,7 +2917,7 @@ class ProductSuiteTab(QWidget): return None account = self._account_for_alias(state.account_alias) if account is None: - self._message("账号不可用", "所选账号不存在,请到④账号管理刷新账号。") + self._message("账号不可用", "所选账号不存在,请到账号管理刷新账号。") return None previous_id = state.project_id try: diff --git a/app/gui/widgets.py b/app/gui/widgets.py index 7801c05..97e5a70 100644 --- a/app/gui/widgets.py +++ b/app/gui/widgets.py @@ -58,9 +58,9 @@ TAB_TITLES = [ "① 导入采集", "② AI生成", "③ 更新蝦皮", - "④ 账号管理", - "⑤ 设置", - "⑥ 商品套图", + "账号管理", + "设置", + "商品套图", ] TAB_STYLE = """ @@ -307,19 +307,6 @@ def _login_status_color(status): return _qcolor(COLOR_MUTED) -def _warning_dot_icon(size=12): - pixmap = QPixmap(size, size) - pixmap.fill(Qt.transparent) - painter = QPainter(pixmap) - painter.setRenderHint(QPainter.Antialiasing) - painter.setPen(Qt.NoPen) - painter.setBrush(QColor(COLOR_WARNING)) - margin = max(1, size // 6) - painter.drawEllipse(margin, margin, size - margin * 2, size - margin * 2) - painter.end() - return QIcon(pixmap) - - def _build_empty_state_card(object_name, button_text="前往账号管理"): card = QWidget() card.setObjectName(object_name) diff --git a/app/gui/workers.py b/app/gui/workers.py index e38e28c..3b3cefa 100644 --- a/app/gui/workers.py +++ b/app/gui/workers.py @@ -980,9 +980,9 @@ class GenerateWorker(BaseWorker): if code_text == "insufficient_points" or "点数不足" in raw: return "点数不足,请先充值" if code_text == "unauthorized" or "api key 无效" in raw.lower(): - return "cmhub API Key 无效,请去⑤设置重填" + return "cmhub API Key 无效,请去设置重填" if code_text in {"model_not_allowed", "no_pricing_rule"} or "模型别名" in raw or "模型配置不可用" in raw: - return "cmhub 模型别名不可用,请去⑤设置刷新别名并保存" + return "cmhub 模型别名不可用,请去设置刷新别名并保存" if code_text == "content_blocked" or "内容安全" in raw: return "cmhub 内容安全策略拒绝本次生成" if code_text == "rate_limited" or "rate_limited" in lowered or "请求过于频繁" in raw: @@ -997,7 +997,7 @@ class GenerateWorker(BaseWorker): or "接口不存在" in raw or str(status or "") == "404" ): - return "cmhub 网关接口不可用,请检查⑤设置中的 Base URL,或联系服务方确认网关版本" + return "cmhub 网关接口不可用,请检查设置中的 Base URL,或联系服务方确认网关版本" if code_text == "upstream_error" or "upstream_error" in lowered or "上游" in raw: return "cmhub 上游生成失败,请稍后重试" if "下载 cmhub 图片失败" in raw: diff --git a/docs/04-architecture.md b/docs/04-architecture.md index 04f75d3..46e0ec6 100644 --- a/docs/04-architecture.md +++ b/docs/04-architecture.md @@ -5,14 +5,14 @@ ## 一、系统结构 -Windows 本地桌面自动化工具,无后端服务。当前正式 GUI 显示 6 个工作流 Tab;⑥商品套图使用新的原生 PySide6 界面,并复用既有 `image_studio_*` 数据与生图服务。 +Windows 本地桌面自动化工具,无后端服务。当前正式 GUI 显示 6 个工作流 Tab;商品套图使用新的原生 PySide6 界面,并复用既有 `image_studio_*` 数据与生图服务。 ```text 运营(人) | v GUI(PySide6 QTabWidget,当前显示 6 Tab) - ① 导入采集 ② AI生成 ③ 更新蝦皮 ④ 账号管理 ⑤ 设置 ⑥ 商品套图 + ① 导入采集 ② AI生成 ③ 更新蝦皮 账号管理 设置 商品套图 | v 核心模块(Python) @@ -24,7 +24,7 @@ GUI(PySide6 QTabWidget,当前显示 6 Tab) ├── chrome 按账号拼启动参数、启动/探测 Chrome、生成快捷方式 ├── cdp CDP 客户端(连接、找/开 tab、执行 JS、拖拽) ├── editor 登录检测 / 采集旧标题旧封面 / 改标题 / 换封面 / 点更新 - ├── ai 文本生成(提示词+旧标题→新标题)/ 图像生成(提示词+旧封面→新封面)/ 商品原图理解(⑥AI帮写) + ├── ai 文本生成(提示词+旧标题→新标题)/ 图像生成(提示词+旧封面→新封面)/ 商品原图理解(商品套图AI帮写) ├── image_studio 商品套图项目/资产/job数据服务(兼容旧AI工场终选) ├── product_suite 套图设置归一化、数量计算、完整提示词与job规划 ├── image_studio_images 远程原图安全下载、本地原图导入、生成图废纸篓 @@ -64,7 +64,7 @@ imported → collected → generated → applied ## 三、职责划分 -**GUI(当前显示 6 Tab)**:见 [routes.md](routes.md)。只做交互与预览,不写业务逻辑;耗时操作走 PySide6 `QObject` worker + `QThread`,用 signal 回主线程刷新 UI。**① 采集点击后会为本轮匹配到的账号自动确保 Chrome 就绪:已打开则复用,未打开才启动;随后只检测登录态,未登录账号的任务跳过并汇总提示去 ④人工登录。③ 更新蝦皮仍是线上提交高风险链路:执行前只检测账号 Chrome/CDP/登录态,不自动启动缺失账号 Chrome。⑥商品套图的只读拉图、原图下载/导入、AI帮写和 cmhub 生成均在独立 worker 中运行;多任务可并行,且不会自动上传蝦皮。** +**GUI(当前显示 6 Tab)**:见 [routes.md](routes.md)。只做交互与预览,不写业务逻辑;耗时操作走 PySide6 `QObject` worker + `QThread`,用 signal 回主线程刷新 UI。**① 采集点击后会为本轮匹配到的账号自动确保 Chrome 就绪:已打开则复用,未打开才启动;随后只检测登录态,未登录账号的任务跳过并汇总提示前往账号管理人工登录。③ 更新蝦皮仍是线上提交高风险链路:执行前只检测账号 Chrome/CDP/登录态,不自动启动缺失账号 Chrome。商品套图的只读拉图、原图下载/导入、AI帮写和 cmhub 生成均在独立 worker 中运行;多任务可并行,且不会自动上传蝦皮。** **核心模块** @@ -76,7 +76,7 @@ imported → collected → generated → applied - `chrome`:拼接启动命令、启动、探测端口、(可选)生成快捷方式。 - `cdp`:连接调试端口、找/开 tab、执行 JS、拖拽、注入文件。 - `editor`:登录检测、**采集**(读旧标题、下载旧封面)、改标题、换封面、点更新。 -- `ai`:`gen_title(prompt, old_title)`、`gen_cover(prompt, old_cover_path)`、`analyze_product_images(instruction, context, image_paths)`;前两者分别负责②标题/生图,后者只供⑥「AI帮写」调用 cmhub 图片理解接口,读取1至8张按 `source_order` 排序的本地商品原图并返回可编辑卖点与白名单计费元数据。 +- `ai`:`gen_title(prompt, old_title)`、`gen_cover(prompt, old_cover_path)`、`analyze_product_images(instruction, context, image_paths)`;前两者分别负责②标题/生图,后者只供商品套图中的「AI帮写」调用 cmhub 图片理解接口,读取1至8张按 `source_order` 排序的本地商品原图并返回可编辑卖点与白名单计费元数据。 - `cmhub_models`:格式化 cmhub 模型别名,并维护仅进程内有效的短期模型目录缓存;缓存键使用规整网关地址和别名,不含 API Key,不写入配置、SQLite、日志或导出文件。 **存储(同一事实只存一处)** @@ -87,7 +87,7 @@ imported → collected → generated → applied - cmhub 模型目录与 AI帮写/正式套图预估价格 → 仅内存短期缓存;预估值只供用户确认,实际扣点仍以网关响应 metadata 为准。 - 业务数据(账号、任务、各阶段结果)→ SQLite `data/cmshopee.db`。 - 图片(采集的旧封面、AI 生成的新封面)→ `data/images/`(路径记在 DB)。 -- 提示词 → 标题当前工作文本存单文件 `data/title_prompt.txt`;标题命名模板存 `data/prompts/title/<名称>.txt`;封面命名模板存 `data/prompts/cover/<名称>.txt`;旧 AI工场模板目录 `data/prompts/image_studio/<名称>.txt` 仅保留兼容,⑥商品套图直接把卖点文本与结构化设置保存在项目表。 +- 提示词 → 标题当前工作文本存单文件 `data/title_prompt.txt`;标题命名模板存 `data/prompts/title/<名称>.txt`;封面命名模板存 `data/prompts/cover/<名称>.txt`;旧 AI工场模板目录 `data/prompts/image_studio/<名称>.txt` 仅保留兼容,商品套图直接把卖点文本与结构化设置保存在项目表。 - 登录态 → 各账号 `data/chrome_user_data_dir//`。 T-538 后统一数据根为 `data/`:打包版默认 `/data`,源码运行默认项目根 `data/`。`config.json` 中 `user_data_root`、`image_dir`、`db_path` 默认仍保存为 `chrome_user_data_dir`、`images`、`cmshopee.db` 等相对值,运行时由 `appconfig` 解析到 `data/` 下;绝对路径作为高级自定义仍按原值使用。启动时会迁移 T-524 旧包的 exe 顶层数据到 `data/`,并检测 `data/` 可写。 @@ -96,7 +96,7 @@ T-538 后统一数据根为 `data/`:打包版默认 `/data`,源 采用**每账号独立 user-data-dir**(非 Chrome profile)。`--remote-debugging-port` 绑定在 user-data-dir/进程上,profile 方案无法每账号独立 CDP、串号风险高。启动主路径用程序 `subprocess` 直启(`--remote-debugging-port` + `--remote-allow-origins=*` + `--user-data-dir`);冷启动登录入口还追加 `--no-first-run`、`--no-default-browser-check`,并把经过校验的 `https:///portal/` 作为唯一启动 URL 放在参数末尾,避免全新 profile 的 `chrome://intro/` 另开首次运行窗口。可选生成 `.lnk` 快捷方式(PowerShell `WScript.Shell`,参数写在「目标」字段),快捷方式不携带上述冷启动专用参数和 URL,保持原行为。 -同一账号重复点击④「启动登录」不得重复执行 `subprocess.Popen`。正确流程是先探测该账号 `debug_port` 的 `/json/version`:端口未响应才按上述启动参数新开 Chrome;CDP 就绪后在最多 2 秒的有界预算内等待初始卖家中心 page target,出现后直接激活。若只出现唯一的 `about:blank`、Chrome 新标签页或全新 profile 的 `chrome://intro/`,则导航并复用该 target;只有完全没有安全可复用 page target 时才兜底创建一次,并返回 `fallback_reason`。端口已响应时复用该账号现有 Chrome/CDP,优先激活卖家中心/登录 tab;没有时才新增一个 tab,绝不改写或关闭用户已有普通页面。④ GUI 在启动期间禁用账号选择和操作,同账号连续触发写 `result=ignored reason=launch_in_progress`,不进入第二次底层启动。 +同一账号重复点击账号管理中的「启动登录」不得重复执行 `subprocess.Popen`。正确流程是先探测该账号 `debug_port` 的 `/json/version`:端口未响应才按上述启动参数新开 Chrome;CDP 就绪后在最多 2 秒的有界预算内等待初始卖家中心 page target,出现后直接激活。若只出现唯一的 `about:blank`、Chrome 新标签页或全新 profile 的 `chrome://intro/`,则导航并复用该 target;只有完全没有安全可复用 page target 时才兜底创建一次,并返回 `fallback_reason`。端口已响应时复用该账号现有 Chrome/CDP,优先激活卖家中心/登录 tab;没有时才新增一个 tab,绝不改写或关闭用户已有普通页面。账号管理界面在启动期间禁用账号选择和操作,同账号连续触发写 `result=ignored reason=launch_in_progress`,不进入第二次底层启动。 ## 五、数据模型 @@ -153,13 +153,13 @@ T-538 后统一数据根为 `data/`:打包版默认 `/data`,源 `ai` 段只放**选择 + 全局生成参数**: -- `backend`:内部字段,取值仍支持 `cmhub` / `direct`;普通产品默认 `cmhub`,⑤设置页不再展示「AI 后端」label 或 direct/cmhub 下拉,保存设置固定写 `cmhub`。`direct` 仅保留为内部兼容/手工回滚路径。 -- `cmhub`:cmhub 网关配置,`base_url` 为网关根地址,保存和请求前会规整为 scheme+host(+port),去掉 `/api`、`/api/v1`、其它路径、查询串和片段;`title_alias` / `image_alias` / `vision_alias` 分别对应②标题、②/⑥正式生图、⑥「AI帮写」图片理解,`vision_alias` 缺失时旧配置迁移为 `vision-standard`,`connect_timeout` 为连接超时;API Key 不在此处保存。⑤设置页展示三类别名和扣点提示,其中图片理解下拉只接受 `operation_type=vision`、`requires_image=true` 且已定价的模型;已保存但暂不可用的值保留并明确提示。⑥正式套图仍只使用已保存的生图 alias,不展示 OpenAI Key、Provider URL、上游接口路径或直连模型 slug。 -- `default_text_model` / `default_image_model`:仅 direct 内部兼容模式下引用 `ai_models.json` 里的模型名(标题用文本模型、封面用图像模型);普通 cmhub 模式不读取这些模型定义,⑤设置页不再展示标题/图片模型角色下拉。 +- `backend`:内部字段,取值仍支持 `cmhub` / `direct`;普通产品默认 `cmhub`,设置页不再展示「AI 后端」label 或 direct/cmhub 下拉,保存设置固定写 `cmhub`。`direct` 仅保留为内部兼容/手工回滚路径。 +- `cmhub`:cmhub 网关配置,`base_url` 为网关根地址,保存和请求前会规整为 scheme+host(+port),去掉 `/api`、`/api/v1`、其它路径、查询串和片段;`title_alias` / `image_alias` / `vision_alias` 分别对应②标题、②与商品套图正式生图、商品套图中的「AI帮写」图片理解,`vision_alias` 缺失时旧配置迁移为 `vision-standard`,`connect_timeout` 为连接超时;API Key 不在此处保存。设置页展示三类别名和扣点提示,其中图片理解下拉只接受 `operation_type=vision`、`requires_image=true` 且已定价的模型;已保存但暂不可用的值保留并明确提示。商品套图正式生成仍只使用已保存的生图 alias,不展示 OpenAI Key、Provider URL、上游接口路径或直连模型 slug。 +- `default_text_model` / `default_image_model`:仅 direct 内部兼容模式下引用 `ai_models.json` 里的模型名(标题用文本模型、封面用图像模型);普通 cmhub 模式不读取这些模型定义,设置页不再展示标题/图片模型角色下拉。 - `generate_mode`:②「生成内容」下拉的主字段,取值 `title` / `cover` / `title_cover`,分别表示只生成标题、只生成封面、生成标题和封面;默认 `title`,避免用户无意产生封面生成成本。 - `generate_cover`:旧兼容字段;保存配置时仍写回,值由 `generate_mode` 推导。旧配置 `false` 会迁移为 `title`,`true` 会迁移为 `title_cover`。GUI 和生成逻辑以 `generate_mode` 为准。 - `resolution`:当前分辨率,下拉 `512 / 1k / 2k / 4k`。 -- `resolution_timeouts`:direct 内部兼容路径使用的分辨率 → **等待大模型返回超时(秒)** 映射。普通默认 `cmhub` 模式下,分辨率只控制生图尺寸;⑤设置页「返回超时」只读展示实际等待口径:标题 600 秒 / 图片 900 秒,不再随分辨率切换显示 180/240/360/600。 +- `resolution_timeouts`:direct 内部兼容路径使用的分辨率 → **等待大模型返回超时(秒)** 映射。普通默认 `cmhub` 模式下,分辨率只控制生图尺寸;设置页「返回超时」只读展示实际等待口径:标题 600 秒 / 图片 900 秒,不再随分辨率切换显示 180/240/360/600。 - direct 内部兼容模式模型本身的定义(url/key/类型/连接超时…)在 `data/config/ai_models.json`,见 5.1b。 - 密钥不在 `config.json`:cmhub API Key 存于 `data/config/cmhub.json`;direct 内部兼容模式每个模型的 `api_key` 存于 `data/config/ai_models.json`。两者均本地明文保存、保存/变更时弹窗提示、UI 打码、gitignore、不入日志/导出。 @@ -173,9 +173,9 @@ T-538 后统一数据根为 `data/`:打包版默认 `/data`,源 该段不是替代 ③ 确认弹窗的常驻授权;③「开始更新」仍必须弹窗确认,用户点是后才执行。`dry_run=true` 时不会真实提交;`dry_run=false` 时以③确认弹窗作为线上提交前的唯一显式确认边界。普通正式更新不读取 `test_item_id` 做阻断。旧配置中的真实提交、封面更新、成功关页等开关只作迁移兼容读取,保存后不再写回。 -`product_suite.last_account_alias` 只保存⑥「商品套图」最近一次有效选择的账号别名。软件重启后的第一个新任务优先恢复该账号;账号已删除或不可用时回退到第一个可用账号。已有账号+商品 ID 项目和恢复的临时草稿始终以 SQLite 中自己的 `account_alias` 为准,不被最近账号覆盖。 +`product_suite.last_account_alias` 只保存商品套图最近一次有效选择的账号别名。软件重启后的第一个新任务优先恢复该账号;账号已删除或不可用时回退到第一个可用账号。已有账号+商品 ID 项目和恢复的临时草稿始终以 SQLite 中自己的 `account_alias` 为准,不被最近账号覆盖。 -`product_suite.last_settings` 只保存⑥「商品套图」最近一次选择的平台、站点、语言和比例,作为软件重启后第一个未绑定商品任务的默认值。已有账号+商品 ID 项目仍以 SQLite `image_studio_projects.suite_settings_json` 为准;同一次运行中新任务优先继承当前任务;绑定已有项目后再由项目设置覆盖最近默认。旧配置缺少该段或值不在当前选项集合时,分别回退到 `Shopee / 中国台湾 / 繁体中文 / 1:1`。`product_suite` 配置不保存商品 ID、卖点提示词、图片路径或密钥。 +`product_suite.last_settings` 只保存商品套图最近一次选择的平台、站点、语言和比例,作为软件重启后第一个未绑定商品任务的默认值。已有账号+商品 ID 项目仍以 SQLite `image_studio_projects.suite_settings_json` 为准;同一次运行中新任务优先继承当前任务;绑定已有项目后再由项目设置覆盖最近默认。旧配置缺少该段或值不在当前选项集合时,分别回退到 `Shopee / 中国台湾 / 繁体中文 / 1:1`。`product_suite` 配置不保存商品 ID、卖点提示词、图片路径或密钥。 ### 5.1b AI 模型清单 `data/config/ai_models.json` @@ -217,7 +217,7 @@ T-538 后统一数据根为 `data/`:打包版默认 `/data`,源 - 文件必须 gitignore,不提交;UI 展示打码。 - `appconfig.load_cmhub_config()` 缺文件时返回空 Key;普通产品默认 backend 仍为 cmhub,但未配置 Key/Base URL/别名时生成阶段会给出清晰配置错误,不静默回退 direct。 -- `backend=cmhub` 但 Base URL、API Key 或别名缺失时,`app/ai.py` 抛 `CMHubError(code="cmhub_not_configured")`,提示去⑤设置配置,不静默回退 direct;cmhub HTTP 404 映射为 `CMHubError(code="not_found")`,提示检查 Base URL 或实例是否已部署 `/api/v1/models`。 +- `backend=cmhub` 但 Base URL、API Key 或别名缺失时,`app/ai.py` 抛 `CMHubError(code="cmhub_not_configured")`,提示去设置配置,不静默回退 direct;cmhub HTTP 404 映射为 `CMHubError(code="not_found")`,提示检查 Base URL 或实例是否已部署 `/api/v1/models`。 - cmhub `/models` 如返回 `display_name/tags/recommended_for/tier/prices`,GUI 优先用这些字段生成中文档位说明;缺少这些字段时按 alias/tag 的保守规则兜底到“默认档”。客户端不得把 OpenAI 原始模型名作为默认执行事实。 ### 5.2 SQLite `cmshopee.db` @@ -235,7 +235,7 @@ CREATE TABLE batches ( deleted_reason TEXT -- 软删除原因/来源 ); --- 账号(④ 账号管理) +-- 账号(账号管理) CREATE TABLE accounts ( id INTEGER PRIMARY KEY, account_name TEXT NOT NULL, -- Shopee 登录账号名(展示/参考) @@ -338,7 +338,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。 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`,记录文件写入、文件锁/保存异常和行数;③ 更新蝦皮 的 `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。 5. **T-563 已接入①采集登录检测容错**:采集前预检和采集中途登录检测遇到 `NO_SESSION_COOKIE`、`LOGIN_CHECK_FAILED`、CDP 短暂读取异常时,应短暂重试并记录 reason、URL、Cookie 名称和重试次数;连续不确定仍不得把同账号后续任务批量标记为 `skipped`。只有明确检测到登录页 URL / `LOGIN_PAGE` 时,才把账号加入需补登录列表并整组略过后续任务。日志不得记录 Cookie 值。 @@ -382,7 +382,7 @@ CREATE TABLE run_log_events ( - 输入列(账号名/别名/商品id)运营填;其余为程序各阶段输出,**回写到原 Excel**(被锁→提示重试/另存副本)。 - 别名以此列为匹配权威,不解析文件名。 -- 标准模板推荐前三列表头固定为:`账号名 | 别名 | 商品id`。不要使用 `账号`、`店铺名`、`店名` 代替匹配列;`别名` 必须与 ④ 账号管理中的 `accounts.alias` 一致。 +- 标准模板推荐前三列表头固定为:`账号名 | 别名 | 商品id`。不要使用 `账号`、`店铺名`、`店名` 代替匹配列;`别名` 必须与 账号管理中的 `accounts.alias` 一致。 - 导入容错:缺少必需列(别名/商品id)时拒绝整个文件,记入 `file_errors`,不导入该文件任何行。 - 脏行处理:单行缺别名/商品id、商品id 格式错误等逐行跳过,计入 `invalid`,不阻塞同文件其他有效行。 @@ -435,27 +435,27 @@ data/images///__new. # AI 生成的新 - **「停止」**:取消未开始的任务,正在跑的少量完成或中断;停止后可再次「开始生成」对剩余继续。 - 进度:标题和图片两条进度分开显示;只生成标题时图片进度显示本轮未生成/0 张,并在运行日志写明本轮生成内容。 - 任一组件生成后 `stage=generated`;**不设逐条人工审核阶段**。若只有标题,③可选择只更新标题;若只有封面,③可选择只更新封面,②标题状态仍为待生成,后续补标题会保留已有封面且不重复生图。双击任务弹窗查看旧封面、新封面和历史候选图;T-577 后弹窗内「重置图片」只清当前任务 `new_cover_path` 并归档旧图,不启动单条 `GenerateWorker`,用户退出后用状态筛选「待生成」批量补生成封面。②「重置生成结果」提供标题/封面/全部的多选或当前筛选范围重置,默认不删除本地新封面文件;已生成且未提交线上的新标题可在②表格本地微调。 -- 并发数、重试、分辨率、jpg 质量、模型/Key 均来自 ⑤ 设置(`data/config.json` 的 `ai` 段;Key 存 `data/config/cmhub.json` 或 direct 兼容清单)。T-547 后标题并发和图片并发都限制为 1..5,失败重试次数限制为 0..10;旧 `config.json` 或手工配置的超限值会在加载/保存时夹紧。⑤仍只展示一个「图片并发」设置;cmhub 模式下②运行日志显示“图片并发 X,cmhub实际生图并发 Y,下载并发 Y”。 -- ⑥商品套图固定使用⑤保存的 cmhub 生图 alias。T-651 后,常规新一轮生成先完成历史确认,再按最终 `build_job_specs()` 在后台读取或复用短期模型目录缓存,最后才显示数量和扣点确认;逐图主图开启时白底图仅使用第一张原图,其他分类按每张原图展开。只有当前生图别名存在唯一无条件 `points_cost` 时显示单张与总预估点数,总数严格按 planned job 数量计算;目录不可用或价格有条件时不显示数字。确认默认、Esc 或关闭均取消,计划在读取期间发生变化时不提交旧 specs;单图重试和恢复不进入该常规确认。预估不落库、不预扣,实际扣点仍以 job 的 cmhub 响应为准。T-637 后套图提示词事实来源分为:安装包只读默认 `app/default_prompts/product_suite/base.txt`、用户全局模板 `data/prompts/product_suite/base.txt`、`app/product_suite.py` 中的结构化上下文与只读规则常量。用户模板首次缺失或为空时,必须先按完整占位符契约校验内置模板,再通过原子写入初始化;已有用户模板不被升级静默覆盖。模板无效时在创建 project/job/worker 和调用 cmhub 前阻断新一轮生成,单张历史重试继续使用原 `image_studio_jobs.prompt` 快照。 -- T-647 后⑥「AI帮写」不是②标题生成的复用入口:它使用 `vision_alias` 调 `POST /api/v1/analyze/images`,把当前项目中1至8张可用本地原图按 `source_order` 在一次请求中发送;超过8张时明确只使用前8张,原图勾选状态不改变该输入集合。视觉提示词将这些图片定义为同一商品项目的多角度、细节、包装或场景证据集,要求产出一份商品级联合分析,不按“图1/图2”逐图罗列;结果按商品概述、可确认卖点、适用人群与场景、套图画面要求、待确认或避免编造的信息组织,并把可见差异明确为待确认项,不强行合并为事实。单图上限10MiB、总计上限32MiB,缺图、未下载、超限或未配置别名时不发请求且保留用户现有卖点。读取等待固定120秒、连接等待沿用⑤设置;读超时或网络中断只提示“结果未确认”,不自动重发。成功只显示图片张数、扣点和余额,不显示路径、base64、接口URL、完整提示词或上游原始响应;取消只在请求前/返回后协作生效,不强杀网络线程。 +- 并发数、重试、分辨率、jpg 质量、模型/Key 均来自设置(`data/config.json` 的 `ai` 段;Key 存 `data/config/cmhub.json` 或 direct 兼容清单)。T-547 后标题并发和图片并发都限制为 1..5,失败重试次数限制为 0..10;旧 `config.json` 或手工配置的超限值会在加载/保存时夹紧。设置仍只展示一个「图片并发」设置;cmhub 模式下②运行日志显示“图片并发 X,cmhub实际生图并发 Y,下载并发 Y”。 +- 商品套图固定使用设置保存的 cmhub 生图 alias。T-651 后,常规新一轮生成先完成历史确认,再按最终 `build_job_specs()` 在后台读取或复用短期模型目录缓存,最后才显示数量和扣点确认;逐图主图开启时白底图仅使用第一张原图,其他分类按每张原图展开。只有当前生图别名存在唯一无条件 `points_cost` 时显示单张与总预估点数,总数严格按 planned job 数量计算;目录不可用或价格有条件时不显示数字。确认默认、Esc 或关闭均取消,计划在读取期间发生变化时不提交旧 specs;单图重试和恢复不进入该常规确认。预估不落库、不预扣,实际扣点仍以 job 的 cmhub 响应为准。T-637 后套图提示词事实来源分为:安装包只读默认 `app/default_prompts/product_suite/base.txt`、用户全局模板 `data/prompts/product_suite/base.txt`、`app/product_suite.py` 中的结构化上下文与只读规则常量。用户模板首次缺失或为空时,必须先按完整占位符契约校验内置模板,再通过原子写入初始化;已有用户模板不被升级静默覆盖。模板无效时在创建 project/job/worker 和调用 cmhub 前阻断新一轮生成,单张历史重试继续使用原 `image_studio_jobs.prompt` 快照。 +- T-647 后商品套图中的「AI帮写」不是②标题生成的复用入口:它使用 `vision_alias` 调 `POST /api/v1/analyze/images`,把当前项目中1至8张可用本地原图按 `source_order` 在一次请求中发送;超过8张时明确只使用前8张,原图勾选状态不改变该输入集合。视觉提示词将这些图片定义为同一商品项目的多角度、细节、包装或场景证据集,要求产出一份商品级联合分析,不按“图1/图2”逐图罗列;结果按商品概述、可确认卖点、适用人群与场景、套图画面要求、待确认或避免编造的信息组织,并把可见差异明确为待确认项,不强行合并为事实。单图上限10MiB、总计上限32MiB,缺图、未下载、超限或未配置别名时不发请求且保留用户现有卖点。读取等待固定120秒、连接等待沿用设置;读超时或网络中断只提示“结果未确认”,不自动重发。成功只显示图片张数、扣点和余额,不显示路径、base64、接口URL、完整提示词或上游原始响应;取消只在请求前/返回后协作生效,不强杀网络线程。 - `product_suite.render_product_suite_prompt()` 是弹窗预览和真实生成的唯一 renderer;`build_job_specs()` 建立本轮 specs 前只读取一次用户模板并冻结,每个 job 保存最终完整 prompt,运行中修改模板只影响下一轮。允许占位符为 `{套图名称}`、`{补充描述}`、平台/地区/语言/比例、可选商品ID/主参考图序号、参考图规则、商品卖点及四个只读规则;必需变量缺失、未知/未闭合花括号、只读规则变量未独占一行都视为无效。四个只读规则覆盖尺寸与长图(含禁止多宫格拼接)、政治标识、价格和尺码;商品主体一致性与禁止编造并入参考图规则。比例仍同时传入 `image_studio_generation.run_jobs(aspect_ratio=...)`,进入 cmhub 请求与输出资产元数据。 -- `image_studio_projects.suite_settings_json` 持久化套图设置,旧数据库由 `db.init_db()` 原位补列,默认 `{}`;`draft_prompt` 继续保存卖点文本。`image_studio_assets` 中有效商品原图最多16张,历史 missing 记录不占有效名额;手工原图不会因再次同步蝦皮 URL 被误标 missing。⑥原图列表的批量勾选只保存在当前 `SuiteTaskState` 对应的界面上下文,不写库;批量移除由 `remove_original_assets_if_unused()` 一次校验项目归属、原图类型和 job/终选引用,并在单个 SQLite 事务中删除资产行、连续重排 `source_order`。服务不删除本地文件或蝦皮线上图片,任一资产校验失败时整批回滚。 +- `image_studio_projects.suite_settings_json` 持久化套图设置,旧数据库由 `db.init_db()` 原位补列,默认 `{}`;`draft_prompt` 继续保存卖点文本。`image_studio_assets` 中有效商品原图最多16张,历史 missing 记录不占有效名额;手工原图不会因再次同步蝦皮 URL 被误标 missing。商品套图原图列表的批量勾选只保存在当前 `SuiteTaskState` 对应的界面上下文,不写库;批量移除由 `remove_original_assets_if_unused()` 一次校验项目归属、原图类型和 job/终选引用,并在单个 SQLite 事务中删除资产行、连续重排 `source_order`。服务不删除本地文件或蝦皮线上图片,任一资产校验失败时整批回滚。 - T-636 起,`image_studio_projects` 增加 `binding_state`(`draft` / `bound`)和稳定 `storage_key`。既有项目迁移为 `bound`,并以原 `item_id` 回填 `storage_key`;项目目录改用 `storage_key`,因此临时草稿绑定正式商品 ID 后不移动目录、不改写已有资产路径。草稿内部使用 `draft_` 作为仅数据库可见的非空 `item_id`,GUI 输入框始终保持空白,用户日志和 cmhub 提示词只使用“临时草稿”或“未绑定商品”,不得暴露该内部值。 -- ⑥已选账号但未填写商品 ID 时允许输入卖点、导入、拖入或粘贴本地图片。非空卖点在现有防抖稳定后会创建一个可恢复临时草稿并保存到 `image_studio_projects.draft_prompt`;首次有效图片导入也会创建草稿。空白卖点、取消选择和全部导入失败不保留空草稿。只有卖点的草稿同样属于可恢复业务内容;卖点清空且没有资产/job 时仍可按空草稿规则清理。草稿可管理本地图片、AI 帮写、生成套图、查看历史和打开结果目录,但在创建 worker、启动 Chrome 或执行 CDP 前禁止「拉取蝦皮主图」。输入合法数字商品 ID 后,经确认原地绑定同一个 `project_id`;资产、job、selection、提示词、套图设置和 `storage_key` 均保持不变。若同账号目标 ID(含软删除项目)已存在则拒绝覆盖或合并。 -- ⑥拉取蝦皮主图使用独立内存 `pull_run_token` 覆盖 URL 读取和本轮后台下载。运行中按钮提供「停止并保留 / 停止并清除本次新增 / 继续拉取」;停止采用协作式安全边界,不强杀线程、Chrome 或已发出的 CDP/HTTP 请求。选择清除时只移除本轮新增、属于当前项目且未被 job/selection 引用的远程原图,并恢复拉取前已有原图的状态和顺序;本地手动导入图片、拉取前已有图片、用户文件和蝦皮线上图片不删除。旧 token 的 URL/下载迟到结果不得覆盖新一轮状态。 +- 在商品套图中,已选账号但未填写商品 ID 时允许输入卖点、导入、拖入或粘贴本地图片。非空卖点在现有防抖稳定后会创建一个可恢复临时草稿并保存到 `image_studio_projects.draft_prompt`;首次有效图片导入也会创建草稿。空白卖点、取消选择和全部导入失败不保留空草稿。只有卖点的草稿同样属于可恢复业务内容;卖点清空且没有资产/job 时仍可按空草稿规则清理。草稿可管理本地图片、AI 帮写、生成套图、查看历史和打开结果目录,但在创建 worker、启动 Chrome 或执行 CDP 前禁止「拉取蝦皮主图」。输入合法数字商品 ID 后,经确认原地绑定同一个 `project_id`;资产、job、selection、提示词、套图设置和 `storage_key` 均保持不变。若同账号目标 ID(含软删除项目)已存在则拒绝覆盖或合并。 +- 商品套图拉取蝦皮主图使用独立内存 `pull_run_token` 覆盖 URL 读取和本轮后台下载。运行中按钮提供「停止并保留 / 停止并清除本次新增 / 继续拉取」;停止采用协作式安全边界,不强杀线程、Chrome 或已发出的 CDP/HTTP 请求。选择清除时只移除本轮新增、属于当前项目且未被 job/selection 引用的远程原图,并恢复拉取前已有原图的状态和顺序;本地手动导入图片、拉取前已有图片、用户文件和蝦皮线上图片不删除。旧 token 的 URL/下载迟到结果不得覆盖新一轮状态。 - 启动时恢复未软删除、至少含一条资产或生成任务的草稿为独立中文“临时草稿”标签,按最近更新时间排序。关闭非空草稿可选择保留、软删除或取消;软删除不物理删除图片目录。③「更新蝦皮」只处理正式任务,不接受临时草稿。 - 第六 Tab 的多个 `SuiteTaskState` 各自保留 generation/pull/import/AI/download worker 与线程引用;切换任务不取消任务。多个任务可并行,但 `image_studio_generation` 使用进程级 semaphore 保证所有套图任务合计最多5个 cmhub 在途 job。线程还在运行时关闭任务只请求协作式停止,模块级引用保留到 `QThread.finished`,不得提前销毁线程对象;下载前后均检查停止信号,停止后的临时文件不入资产库。 - T-639 后每轮套图生成使用仅存在内存的 `run_token` 隔离迟到信号,progress/finished/cancelled/failed 通过主线程绑定槽统一处理;正常 worker 结果、`QThread.finished` 和本轮 job 连续两次全部终态看门狗共同进入幂等 finalize。GUI 只按本轮明确 `job_ids` 判断完成,不用历史图片数量;即使最终信号丢失也会恢复按钮,旧线程引用仍保留到真实结束。停止为协作式:调度循环约每200ms检查标记并取消未开始 future,提交/轮询在有界请求返回后停止;requests 在流式数据块边界取消,Windows curl 由隐藏窗口 `Popen` 有界 terminate/kill。已有 `task_id` 的停止任务保留 resume,不假设服务端任务被取消或点数退回。 -- T-643 后 `image_studio_projects.current_generation_round_key` 是⑥主结果区的持久化当前轮次;每次常规「生成套图」创建独立 UUID `generation_round_key`,并为每条 `image_studio_jobs` 写入稳定的 `generation_slot_index`。内存 `run_token` 仍只用于线程和迟到信号隔离,不承担业务轮次语义。只有一轮中至少一条 job 成功时,才在同一 SQLite 事务内将该轮提升为当前轮;全失败或全取消保留原当前轮,部分成功则保留成功、失败槽位供后续重试。单张重试新建 job 但继承原轮次和槽位,当前展示按同轮同槽位最新 job、槽位顺序读取;所有尝试、错误、计费和输出资产均保留。旧 job 的轮次/槽位保持 NULL,查询层统一作为“旧版历史记录”,不以时间、文件名或数量猜测轮次;下一次成功的新格式整轮才建立当前轮。 -- T-646 后⑥主结果区仍只显示项目的当前轮次;「历史生成」改为全局非模态只读窗口,默认跨所有未软删除商品项目按轮次创建时间倒序读取(同一时间以稳定 job ID 补序),首屏及每次翻页最多30轮。窗口只读取 SQLite 的轮次摘要、每槽最新有效 job 和受限尺寸缩略图,不扫描图片目录或一次加载原图;店铺/账号、商品 ID 与“仅当前商品”筛选都在查询层完成,当前商品快捷筛选默认关闭。正常 `generation_round_key` 一轮一行,单张重试只更新该槽位当前状态并计入重试数,不新增历史行;NULL 轮次继续作为“旧版历史记录”兼容,不猜测轮次边界。每行最多显示5张缩略图,余量显示 `+N`;文件缺失只显示中文占位,不删除 DB。双击缩略图从被点图片打开该轮所有可用图片的自适应原尺寸浏览;“导出本轮”在后台仅复制成功且本地存在的输出 asset 到用户选择目录下安全命名的新子目录,确定性追加序号避免覆盖,不移动/重命名/删除内部 asset。窗口不提供批量导出、删除、重试、设为当前轮或重新生成;关闭商品任务不关闭全局窗口,应用退出时协作停止导出并释放窗口资源,生成 worker 不受历史窗口影响。 +- T-643 后 `image_studio_projects.current_generation_round_key` 是商品套图主结果区的持久化当前轮次;每次常规「生成套图」创建独立 UUID `generation_round_key`,并为每条 `image_studio_jobs` 写入稳定的 `generation_slot_index`。内存 `run_token` 仍只用于线程和迟到信号隔离,不承担业务轮次语义。只有一轮中至少一条 job 成功时,才在同一 SQLite 事务内将该轮提升为当前轮;全失败或全取消保留原当前轮,部分成功则保留成功、失败槽位供后续重试。单张重试新建 job 但继承原轮次和槽位,当前展示按同轮同槽位最新 job、槽位顺序读取;所有尝试、错误、计费和输出资产均保留。旧 job 的轮次/槽位保持 NULL,查询层统一作为“旧版历史记录”,不以时间、文件名或数量猜测轮次;下一次成功的新格式整轮才建立当前轮。 +- T-646 后商品套图主结果区仍只显示项目的当前轮次;「历史生成」改为全局非模态只读窗口,默认跨所有未软删除商品项目按轮次创建时间倒序读取(同一时间以稳定 job ID 补序),首屏及每次翻页最多30轮。窗口只读取 SQLite 的轮次摘要、每槽最新有效 job 和受限尺寸缩略图,不扫描图片目录或一次加载原图;店铺/账号、商品 ID 与“仅当前商品”筛选都在查询层完成,当前商品快捷筛选默认关闭。正常 `generation_round_key` 一轮一行,单张重试只更新该槽位当前状态并计入重试数,不新增历史行;NULL 轮次继续作为“旧版历史记录”兼容,不猜测轮次边界。每行最多显示5张缩略图,余量显示 `+N`;文件缺失只显示中文占位,不删除 DB。双击缩略图从被点图片打开该轮所有可用图片的自适应原尺寸浏览;“导出本轮”在后台仅复制成功且本地存在的输出 asset 到用户选择目录下安全命名的新子目录,确定性追加序号避免覆盖,不移动/重命名/删除内部 asset。窗口不提供批量导出、删除、重试、设为当前轮或重新生成;关闭商品任务不关闭全局窗口,应用退出时协作停止导出并释放窗口资源,生成 worker 不受历史窗口影响。 - T-648 后,用户发起一轮常规「生成套图」前,服务层仅根据 SQLite 中当前项目每个稳定槽位的最新有效成功 job(NULL 旧版轮次仍按一轮兼容)汇总成功轮数、图片数和最近成功时间;不扫描生成目录。若摘要非空,GUI 先显示默认取消的「已有套图生成记录」三选一确认:查看历史仅打开同一全局历史窗口并临时勾选“仅当前商品”,不创建轮次或请求;继续生成新一轮才进入原有费用确认,并在最终确认后生成新的 UUID 轮次;取消或关闭不写 job、asset、轮次或计费。单张重试、恢复未完成任务及只有失败/取消记录的项目不经过该确认。 -- T-649 后,历史窗口的店铺筛选改为结构化下拉:首项固定为「全部店铺」,当前账号按「账号名(账号别名)」显示;未软删除历史项目中仍存在、但账号管理已删除的别名按「历史店铺:别名(账号已删除)」保留。下拉值始终是稳定的 `account_alias`,查询使用 `projects.account_alias = ?` 精确匹配;商品 ID 与“仅当前商品”可叠加,刷新时保留仍存在的选择。⑥顶部不再提供打开内部生成目录的按钮,用户导出只从历史记录的「导出本轮」进入外部目标目录;结果卡的单图文件操作保持不变。 +- T-649 后,历史窗口的店铺筛选改为结构化下拉:首项固定为「全部店铺」,当前账号按「账号名(账号别名)」显示;未软删除历史项目中仍存在、但账号管理已删除的别名按「历史店铺:别名(账号已删除)」保留。下拉值始终是稳定的 `account_alias`,查询使用 `projects.account_alias = ?` 精确匹配;商品 ID 与“仅当前商品”可叠加,刷新时保留仍存在的选择。商品套图顶部不再提供打开内部生成目录的按钮,用户导出只从历史记录的「导出本轮」进入外部目标目录;结果卡的单图文件操作保持不变。 提示词管理: - **标题提示词**:`data/title_prompt.txt` 是当前工作文本,「保存标题提示词」写入该文件;软件启动时加载该文件回显到输入框(缺失则空)。标题命名模板另存于 `data/prompts/title/*.txt`,左侧模板行提供下拉、新建、保存模板、另存为、重命名、删除;模板只负责被用户选中后载入编辑框,或把当前编辑框内容保存为命名模板,不改变生成读取路径,也不在启动时覆盖 `title_prompt.txt`。T-549 后标题提示词支持 `{旧标题}` 占位符:若提示词含 `{旧标题}`,生成前替换为该任务旧标题且不再自动追加旧标题块;若不含,则保持旧行为自动追加“旧标题:...”块。两种情况都会保留“请只返回新标题,不要解释。”输出约束。 - **封面提示词**:多模板。左侧模板行提供下拉(读 `data/prompts/cover/*.txt`)、新建、保存模板、模板操作(另存为/重命名/删除);重名校验、删除二次确认、删空给默认。 -- **商品套图提示词**:单一全局基础模板,用户文件为 `data/prompts/product_suite/base.txt`,内置恢复源为 `app/default_prompts/product_suite/base.txt`。⑥「商品卖点与要求」标题行提供「提示词设置」;弹窗左侧编辑模板、右侧按白底图或其他当前分类只读预览最终请求,编辑使用 200ms 防抖。保存与「恢复默认」共用同一个 UTF-8 原子写入服务;恢复默认会先校验内置资源并经确认立即落盘。用户可移动只读规则占位符位置,但不能修改规则内容或删除必需变量;预览和新 job prompt 必须逐字一致。 +- **商品套图提示词**:单一全局基础模板,用户文件为 `data/prompts/product_suite/base.txt`,内置恢复源为 `app/default_prompts/product_suite/base.txt`。商品套图中的「商品卖点与要求」标题行提供「提示词设置」;弹窗左侧编辑模板、右侧按白底图或其他当前分类只读预览最终请求,编辑使用 200ms 防抖。保存与「恢复默认」共用同一个 UTF-8 原子写入服务;恢复默认会先校验内置资源并经确认立即落盘。用户可移动只读规则占位符位置,但不能修改规则内容或删除必需变量;预览和新 job prompt 必须逐字一致。 - **变量**:标题提示词本阶段只支持 `{旧标题}`,左侧按钮「插入旧标题」在标题提示词光标处插入 `{旧标题}`。封面提示词支持占位符 `{旧标题}`、`{新标题}`、`{商品id}`、`{店铺}`,生成前用该任务真实值替换(`render_prompt`)。「插入标题」= 在封面提示词光标处插入 `{新标题}`;「预览」= 用某条任务的值替换封面变量后展示,确认实际发送给 AI 的内容。 ### 6.3 应用更新(③ Tab) @@ -465,13 +465,13 @@ data/images///__new. # AI 生成的新 - ③ 提供「检查本轮更新」按钮:只读取当前筛选结果和写运行日志,不打开 Shopee、不提交、不改任务状态;检查汇总展示总数、店铺分布、每批最大条数、预计批次数、更新内容和略过原因。 - 弹确认前先读取 `data/config.json` 的 `shopee_update` 执行参数。普通正式更新不再检查 `test_item_id` 或旧真实提交开关,当前筛选结果可以包含多个真实商品 ID。`max_items_per_run` 作为每批最大任务数,当前筛选总数超过该值时自动分批,不再按总数阻断。 - 用户在③确认弹窗点「是/确认」才开始批量更新;点「否/取消」不执行、不改库。 -- 真实更新前必须做账号就绪预检:按当前筛选结果汇总需要的账号;无账号、账号 Chrome 未启动、CDP 端口不可访问、未登录或本轮账号端口冲突时,整体返回 `blocked` 并由 GUI 弹窗列出账号/原因、引导去④账号管理。预检不通过时不创建商品编辑页、不调用 `editor.apply_task()`、不写失败状态、不自动调用「启动登录」或静默打开 Chrome。 +- 真实更新前必须做账号就绪预检:按当前筛选结果汇总需要的账号;无账号、账号 Chrome 未启动、CDP 端口不可访问、未登录或本轮账号端口冲突时,整体返回 `blocked` 并由 GUI 弹窗列出账号/原因、引导去账号管理。预检不通过时不创建商品编辑页、不调用 `editor.apply_task()`、不写失败状态、不自动调用「启动登录」或静默打开 Chrome。 - 对确认后的**已生成(generated)任务**:按③「更新内容」模式执行 `open_product` → `change_title(new_title)`(选择标题时)→ `replace_cover(new_cover_path)`(选择封面时)→ `click_update` 提交。选择 `只更新标题` 时即使任务有 `new_cover_path` 也不会替换封面;选择 `只更新封面` 时即使任务有 `new_title` 也不会改标题。 - `open_product` 打开商品详情页失败时,要把页面 toast 中的错误原因上浮到③运行日志和任务失败原因;商品 ID 失效、无权限或店铺不匹配时应能看到 Shopee 原始提示,而不是只看到等待详情页超时;如果此时 tab 是本轮自动新建的,`open_product` 要负责关闭该失败 tab。 - `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` 非空且文件存在),再删除当前线上第一张图、等待图片管理器稳定、上传新图并拖到第一位。缺失备份时不删除线上第一张图,直接返回明确错误,要求先回到①采集旧封面或修复本地备份。 -- 默认串行、单条失败继续;⑤「同时更新蝦皮账号」设为 `1` 时逐个账号执行,设为 `2..5` 时按账号分组并行,不同账号可同时跑,同一账号内仍串行。真实更新前检查本轮账号 `debug_port`,端口冲突直接阻断。 +- 默认串行、单条失败继续;设置「同时更新蝦皮账号」设为 `1` 时逐个账号执行,设为 `2..5` 时按账号分组并行,不同账号可同时跑,同一账号内仍串行。真实更新前检查本轮账号 `debug_port`,端口冲突直接阻断。 - 「检查本轮更新」只写 `run_logs/run_log_events` 和弹窗/状态栏检查汇总,不调用 `editor.apply_task()`,不做账号登录预检,不写任务状态,不回写 Excel。 - 真实更新按每批最大条数分批执行,每条立即写 SQLite;全部完成回写 Excel(新字段+状态)+ 弹窗汇总。真实更新与检查都写运行日志,日志 payload 走脱敏工具。 - 分批更新停止语义为协作式停止:点击停止后设置取消标记;当前正在执行的商品跑到安全边界后写库结束,不再开始新商品,也不进入下一批。未开始任务保持原状态,后续可继续。 @@ -482,8 +482,8 @@ data/images///__new. # AI 生成的新 - 无 Shopee tab 时打开卖家中心根地址 `https:///`(默认 `https://seller.shopee.tw/`);不自动登录。重定向到登录页返回明确 `LOGIN_PAGE`;页面 target 可用且 Cookie API 至少成功一次、但确实缺少 `SPC_ST`/`SPC_U` 时返回 `NO_SESSION_COOKIE`;websocket/target 正在关闭、整个检测周期一次 Cookie 都未成功读取时返回 `LOGIN_CHECK_TARGET_UNAVAILABLE`,界面显示“登录状态暂不可确认”,不得误报账号退出登录。 - 多个 Shopee page target 并存时优先稳定的卖家中心 portal 页面,再尝试商品页,显式登录页最后判定;单个 target 连接或 Cookie 读取失败后立即重枚举/改连其他未尝试 target,不等待完整 8 秒才交给外层重试。①采集的 retry/recovered 日志必须带当前任务 ID、商品 ID;关闭上一条商品页的 target 仍短暂出现在 `/json` 时,不得把上一条 URL 误记为下一条账号掉线。 -- ④「启动登录」只负责准备该账号独立 user-data-dir + CDP 端口的 Chrome,供用户人工登录;该入口必须幂等:若端口已响应,复用现有账号 Chrome 并打开/激活卖家中心登录 tab,不再新开 Chrome;若端口未响应,才用卖家中心初始 URL 启动 Chrome,并优先复用本轮初始 page target,避免空白窗口后再建第二页。返回结果用 `created_tab/startup_target_reused/startup_page_navigated/page_target_count/fallback_reason` 说明实际路径,`chrome_launch` 日志同步记录且不得包含密码、Cookie 或 token。「检测登录」只验证当前 Chrome/CDP/会话 Cookie 是否可用。① 采集的预检会复用同一幂等启动能力,自动确保本轮匹配账号 Chrome 就绪但不自动登录;③ 更新的预检只检测,不自动启动缺失浏览器。若商品详情页或卖家中心重定向到 `accounts.shopee.tw/seller/login`,必须按登录页处理,返回 `LOGIN_PAGE` 并在 GUI 显示未登录。 -- T-585 后新配置的 `chrome_path` 默认留空。程序在数据目录就绪、强制升级检查通过后、创建主窗口前,仅当当前路径为空或不可启动时,按当前用户/本机 Windows App Paths(含 64/32 位视图)、标准 Chrome 目录和 `PATH` 自动寻找 `chrome.exe`;命中才持久化归一化绝对路径,并在状态栏显示“已自动定位 Chrome”。有效的自定义/便携版路径和可从 PATH 启动的 `chrome.exe` 不覆盖;不扫描整盘、不检测 Edge 或 Chromium。⑤的“自动检测”只填入输入框并标记未保存,仍由用户点“保存设置”确认写入。 +- 账号管理中的「启动登录」只负责准备该账号独立 user-data-dir + CDP 端口的 Chrome,供用户人工登录;该入口必须幂等:若端口已响应,复用现有账号 Chrome 并打开/激活卖家中心登录 tab,不再新开 Chrome;若端口未响应,才用卖家中心初始 URL 启动 Chrome,并优先复用本轮初始 page target,避免空白窗口后再建第二页。返回结果用 `created_tab/startup_target_reused/startup_page_navigated/page_target_count/fallback_reason` 说明实际路径,`chrome_launch` 日志同步记录且不得包含密码、Cookie 或 token。「检测登录」只验证当前 Chrome/CDP/会话 Cookie 是否可用。① 采集的预检会复用同一幂等启动能力,自动确保本轮匹配账号 Chrome 就绪但不自动登录;③ 更新的预检只检测,不自动启动缺失浏览器。若商品详情页或卖家中心重定向到 `accounts.shopee.tw/seller/login`,必须按登录页处理,返回 `LOGIN_PAGE` 并在 GUI 显示未登录。 +- T-585 后新配置的 `chrome_path` 默认留空。程序在数据目录就绪、强制升级检查通过后、创建主窗口前,仅当当前路径为空或不可启动时,按当前用户/本机 Windows App Paths(含 64/32 位视图)、标准 Chrome 目录和 `PATH` 自动寻找 `chrome.exe`;命中才持久化归一化绝对路径,并在状态栏显示“已自动定位 Chrome”。有效的自定义/便携版路径和可从 PATH 启动的 `chrome.exe` 不覆盖;不扫描整盘、不检测 Edge 或 Chromium。设置的“自动检测”只填入输入框并标记未保存,仍由用户点“保存设置”确认写入。 ## 七、CDP 已验证事实(务必遵守) @@ -492,9 +492,9 @@ data/images///__new. # AI 生成的新 | Chrome 启动参数 | 全关后带 `--remote-debugging-port= --remote-allow-origins=* --user-data-dir=`;缺 allow-origins 则 WebSocket 403 | | 代理干扰 | 清除 `*_proxy`(requests `trust_env=False`),否则连本地 CDP 超时 | | WebSocket Origin | `websocket-client` `suppress_origin=True` | -| 关闭连接 vs 关闭 tab | `CDP.close()` 只关闭 WebSocket;需要关闭浏览器页面时必须调用浏览器 target 关闭接口。①采集和⑥只读商品页关闭本轮自动新建 target 后,最多等待 2 秒确认 target 从 `/json` 消失;超时只记诊断,不覆盖成功结果,复用的用户已有 tab 不关闭。③ 更新时程序自动新建的商品页成功/失败都关闭,成功提交且确认跳回商品列表页时关闭前等待 2 秒;③ 复用用户已有商品页时不关闭页面 | +| 关闭连接 vs 关闭 tab | `CDP.close()` 只关闭 WebSocket;需要关闭浏览器页面时必须调用浏览器 target 关闭接口。①采集和商品套图只读商品页关闭本轮自动新建 target 后,最多等待 2 秒确认 target 从 `/json` 消失;超时只记诊断,不覆盖成功结果,复用的用户已有 tab 不关闭。③ 更新时程序自动新建的商品页成功/失败都关闭,成功提交且确认跳回商品列表页时关闭前等待 2 秒;③ 复用用户已有商品页时不关闭页面 | | 登录 target 竞态 | `/json/close/` 返回成功不代表 target 已立即从 `/json` 消失。登录检测不得固定使用枚举到的第一个 Shopee page;连接或 Cookie API 因 target 销毁失败时应快速重选有效页面。只有 Cookie API 成功返回且确实缺会话 Cookie 才是 `NO_SESSION_COOKIE`;一次都未成功读取是 `LOGIN_CHECK_TARGET_UNAVAILABLE` | -| 前台激活 | ①采集和⑥商品套图只读打开商品页时不主动 `Page.bringToFront`;新建 tab 尝试 `Target.createTarget(background=true)`,不支持时退回普通新建。③更新真实提交每条任务都以前台方式新建或激活商品 tab,并执行 `Page.bringToFront`,保障上传、图片管理器刷新和拖拽排序稳定;后台态封面恢复逻辑仅保留给兼容直接调用,不作为正常③批量路径 | +| 前台激活 | ①采集和商品套图只读打开商品页时不主动 `Page.bringToFront`;新建 tab 尝试 `Target.createTarget(background=true)`,不支持时退回普通新建。③更新真实提交每条任务都以前台方式新建或激活商品 tab,并执行 `Page.bringToFront`,保障上传、图片管理器刷新和拖拽排序稳定;后台态封面恢复逻辑仅保留给兼容直接调用,不作为正常③批量路径 | | SPA 就绪 | 不用 load 事件;轮询“唯一商品名称输入框 + 至少一张主图 itembox + 唯一主图上传输入框”三者都在。脚本返回标题命中数、主图/上传入口状态和当前 URL 的就绪快照;超时错误必须指出具体缺失组件,不能只报泛化超时 | | 商品页错误 toast | Shopee 错误提示使用 `.eds-toasts` / `.eds-toast__content`,可能很快隐藏或 `display:none`。打开商品页/等待 SPA 就绪前应注入 `MutationObserver` 或等价监听,把 toast 文本、`outerHTML`、当前 URL、时间、可见状态保存到页面缓存(如 `window.__cmshopee_toasts`);等待详情页关键元素超时时,再兜底读取当前 DOM 中的 toast。明确商品失效/不存在/无权限类 toast 即使已经隐藏,也优先成为 `open_product` 失败原因并驱动①阶段列显示“商品失效”;其他普通 toast 只有仍可见且属于当前页面 URL 时,才以“页面提示(可能无关)”附加在就绪快照后。已隐藏的物流、备货、库存、价格等编辑校验提示不得覆盖真正缺失的就绪组件;网络、CDP、未登录、页面超时、风控等其他失败仍显示“失败” | @@ -514,11 +514,11 @@ data/images///__new. # AI 生成的新 ## 八、推荐开发顺序 1. **地基**:先建 `app/` 包、`app/cdp.py`、`app/__main__.py`、根目录 `main.py`;再做 `app/editor.py`(含采集)、`app/appconfig.py`+`data/config.json`、`app/db.py`、`.gitignore`。 -2. **账号与启动**(④):`config` 建目录、`chrome` 启动器/快捷方式、登录保活与检测、账号 CRUD。 +2. **账号与启动**(账号管理):`config` 建目录、`chrome` 启动器/快捷方式、登录保活与检测、账号 CRUD。 3. **导入采集**(①):`excel` 导入、采集旧标题/旧封面、回写。 4. **AI 生成**(②):`ai` 模块、提示词、对照预览。 5. **更新蝦皮**(③):对已生成任务弹窗批量确认后换标题+封面、提交、回写。 -6. **设置**(⑤)+ 首次未配账号引导保护。 +6. **设置** + 首次未配账号引导保护。 ## 九、项目结构建议 diff --git a/docs/api.md b/docs/api.md index 56b0eb0..326f5d6 100644 --- a/docs/api.md +++ b/docs/api.md @@ -40,7 +40,7 @@ cmhub_request_url(base_url, endpoint) -> str # 先规整 base_url,再拼 /ap response_timeout(config=None) -> int # = resolution_timeouts[resolution](返回超时,随分辨率) ``` -`default_config()` / `load_config()` 包含 `shopee_update` 执行配置段:历史/调试兼容测试商品 ID、更新内容模式 `update_mode`、每批最大更新条数、内部兼容 `dry_run`、同时更新蝦皮账号数 `max_parallel_accounts`。普通正式更新不再用测试商品 ID 或旧真实提交开关阻断当前筛选结果;封面是否参与本轮更新由③「更新内容」下拉决定;线上提交前的显式确认边界是③「开始更新」确认弹窗。`product_suite.last_settings` 只保存⑥最近选择的平台/站点/语言/比例,供未绑定商品的新任务初始化;已有项目自己的 `suite_settings_json` 优先。`config.json` 不保存 AI Key;写入 `api_key` / `*_key` / `token` / `*_token` / `password` / `*_password` 等敏感字段时抛 `ConfigError`。普通产品默认 cmhub,AI Key 存 `data/config/cmhub.json`;`data/config/ai_models.json` 仅为 direct 内部兼容路径。T-538 后,配置中默认仍保存 `chrome_user_data_dir`、`images`、`cmshopee.db` 等相对值,运行时解析到 `data/` 下,保持免安装目录可移动。 +`default_config()` / `load_config()` 包含 `shopee_update` 执行配置段:历史/调试兼容测试商品 ID、更新内容模式 `update_mode`、每批最大更新条数、内部兼容 `dry_run`、同时更新蝦皮账号数 `max_parallel_accounts`。普通正式更新不再用测试商品 ID 或旧真实提交开关阻断当前筛选结果;封面是否参与本轮更新由③「更新内容」下拉决定;线上提交前的显式确认边界是③「开始更新」确认弹窗。`product_suite.last_settings` 只保存商品套图最近选择的平台/站点/语言/比例,供未绑定商品的新任务初始化;已有项目自己的 `suite_settings_json` 优先。`config.json` 不保存 AI Key;写入 `api_key` / `*_key` / `token` / `*_token` / `password` / `*_password` 等敏感字段时抛 `ConfigError`。普通产品默认 cmhub,AI Key 存 `data/config/cmhub.json`;`data/config/ai_models.json` 仅为 direct 内部兼容路径。T-538 后,配置中默认仍保存 `chrome_user_data_dir`、`images`、`cmshopee.db` 等相对值,运行时解析到 `data/` 下,保持免安装目录可移动。 敏感信息展示/日志辅助: @@ -60,8 +60,8 @@ save_cmhub_config(config, path="data/config/cmhub.json") -> dict get_cmhub_api_key(path="data/config/cmhub.json", masked=False) -> str ``` -`data/config.json` 只保存 `ai.backend`、`ai.cmhub.base_url/title_alias/image_alias/vision_alias/connect_timeout` 等非密钥配置;`vision_alias` 默认 `vision-standard`,供⑥「AI帮写」独立使用。T-529 后普通设置页固定保存 `ai.backend=cmhub`,不暴露后端切换;`data/config/cmhub.json` 必须 gitignore,展示时打码,不写日志/导出。 -AI 模型清单(`data/config/ai_models.json`,含本地明文密钥,已建;UI 由 ⑤ 设置复用): +`data/config.json` 只保存 `ai.backend`、`ai.cmhub.base_url/title_alias/image_alias/vision_alias/connect_timeout` 等非密钥配置;`vision_alias` 默认 `vision-standard`,供商品套图中的「AI帮写」独立使用。T-529 后普通设置页固定保存 `ai.backend=cmhub`,不暴露后端切换;`data/config/cmhub.json` 必须 gitignore,展示时打码,不写日志/导出。 +AI 模型清单(`data/config/ai_models.json`,含本地明文密钥,已建;UI 由 设置复用): ```python default_ai_models_config() -> dict @@ -179,7 +179,7 @@ export_copy(batch_id, out_dir_or_path, path=None) -> dict # 退路:另存带旧字段的副本,不动原文件;目录输出时生成 *_cmshopee回写.xlsx ``` -列模板见 [架构 5.3](04-architecture.md);别名以“别名”列为权威,必须与 ④ 账号管理中的账号别名一致。`shopee待处理任务模板.xlsx` 是可提交的标准空模板;运营填写后的 Excel 副本属于业务数据,不提交。 +列模板见 [架构 5.3](04-architecture.md);别名以“别名”列为权威,必须与 账号管理中的账号别名一致。`shopee待处理任务模板.xlsx` 是可提交的标准空模板;运营填写后的 Excel 副本属于业务数据,不提交。 ## config 模块(`app/config.py`,已建) @@ -194,7 +194,7 @@ ensure_user_data_dir(slug, root=None, config=None) -> str ## accounts 模块(`app/accounts.py`,已建) -账号管理服务层,供 ④ Tab 调用;不操作 QWidget,不自动登录/填密码。 +账号管理服务层,供 账号管理 Tab 调用;不操作 QWidget,不自动登录/填密码。 ```python class AccountError(RuntimeError): ... @@ -223,7 +223,7 @@ login_status_text(status) -> str - `debug_port` 在账号服务层按账号唯一校验;默认端口取 `debug_port_range` 中第一个未占用端口。 - `delete_account()` 只删除 DB 账号记录,不删除本地 user-data-dir,避免误删登录态。 - `launch_for_login()` 是幂等入口。先检查 `chrome.is_running(account.debug_port)`;若该账号 CDP 端口已响应,不再调用 `subprocess.Popen`,而是复用已打开的账号 Chrome,优先激活/打开 `https:///portal/` 或卖家中心登录 tab,并返回 `reused=true`,且不会改写已有普通页面。若端口未响应,才启动带该账号 user-data-dir、CDP 端口和唯一卖家中心初始 URL 的 Chrome;CDP 就绪后短时等待并复用初始 target,只有无安全可复用 page target 时才兜底创建一次。返回值除 `action/pid/target_id/url/created_tab` 外,还带 `initial_url/startup_target_reused/startup_page_navigated/page_target_count/fallback_reason/target_probe_error` 供日志诊断。不会读取、填写或提交密码,不绕过验证码。 -- ③ 更新蝦皮的账号预检不会自动调用 `launch_for_login()`;Chrome 未启动/端口不可达时只返回阻断原因,由 GUI 提示用户去④手动打开账号浏览器并登录。T-105b 的复用逻辑只作用于④用户主动点击「启动登录」这一入口。 +- ③ 更新蝦皮的账号预检不会自动调用 `launch_for_login()`;Chrome 未启动/端口不可达时只返回阻断原因,由 GUI 提示用户前往账号管理手动打开账号浏览器并登录。T-105b 的复用逻辑只作用于用户在账号管理中主动点击「启动登录」这一入口。 - `create_shortcut()` 生成 `.lnk`,目标/参数复用 `chrome.build_launch_args()`,不包含密码。 - `detect_login()` 复用 `editor.login_status()`;检测为已登录时更新 `last_login_at`。 @@ -305,7 +305,7 @@ gen_cover(cover_prompt, old_cover_path, out_path, resolution=None, jpg_quality=N # 支持返回 url / data URL / b64_json,按 resolution resize 并以 jpg_quality 保存 JPEG,返回路径;新生成默认写入 `image_dir///__new.jpg`,历史 DB 已存路径继续按原路径读取 analyze_product_images(instruction, context, image_paths, config=None, cmhub_config_path="data/config/cmhub.json", on_event=None) -> dict -# 仅⑥商品套图AI帮写使用;backend 必须为 cmhub。按输入顺序校验1..8张本地图片(单张<=10MiB、合计<=32MiB),在一次 POST /api/v1/analyze/images 中作为同商品多角度证据集发送;固定提示词要求一份商品级联合分析,不输出逐图编号,并按商品概述/可确认卖点/人群场景/套图画面/待确认信息组织文本。返回 {text, image_count, metadata};metadata 只允许 alias/model_used/points_cost/points_balance/call_id。 +# 仅商品套图AI帮写使用;backend 必须为 cmhub。按输入顺序校验1..8张本地图片(单张<=10MiB、合计<=32MiB),在一次 POST /api/v1/analyze/images 中作为同商品多角度证据集发送;固定提示词要求一份商品级联合分析,不输出逐图编号,并按商品概述/可确认卖点/人群场景/套图画面/待确认信息组织文本。返回 {text, image_count, metadata};metadata 只允许 alias/model_used/points_cost/points_balance/call_id。 generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=None) -> dict fetch_cmhub_models(base_url, api_key, connect_timeout=10, read_timeout=30) -> list[dict] @@ -320,11 +320,11 @@ fetch_cmhub_models(base_url, api_key, connect_timeout=10, read_timeout=30) -> li 要点: - `backend=direct`:内部兼容/手工回滚路径;标题用 `default_text_model`、封面用 `default_image_model`(`appconfig.get_model` 取定义,含 url/key/api_type)。 -- `backend=cmhub`:普通产品默认路径;标题调用 `POST /api/v1/generate/title`;②批量封面生成调用 `POST /api/v1/generate/image/tasks` + `GET /api/v1/generate/image/tasks/{task_id}`,模型字段使用 `ai.cmhub.title_alias/image_alias`,Key 来自 `data/config/cmhub.json`。⑥「AI帮写」单独调用 `POST /api/v1/analyze/images`,只使用 `ai.cmhub.vision_alias`,不得回退或混用生文/生图别名;同一商品项目的原图在一条请求内联合理解,用户勾选状态不参与选图。`gen_cover()` 单独调用没有任务/DB 上下文,第一版保留旧同步 `POST /api/v1/generate/image` 兼容路径。 +- `backend=cmhub`:普通产品默认路径;标题调用 `POST /api/v1/generate/title`;②批量封面生成调用 `POST /api/v1/generate/image/tasks` + `GET /api/v1/generate/image/tasks/{task_id}`,模型字段使用 `ai.cmhub.title_alias/image_alias`,Key 来自 `data/config/cmhub.json`。商品套图中的「AI帮写」单独调用 `POST /api/v1/analyze/images`,只使用 `ai.cmhub.vision_alias`,不得回退或混用生文/生图别名;同一商品项目的原图在一条请求内联合理解,用户勾选状态不参与选图。`gen_cover()` 单独调用没有任务/DB 上下文,第一版保留旧同步 `POST /api/v1/generate/image` 兼容路径。 - 标题提示词组装:`gen_title()` 的 direct 与 cmhub 路径共用标题 prompt 规则。若标题提示词包含 `{旧标题}`,生成前替换为该任务旧标题,不再自动追加旧标题块;若不包含 `{旧标题}`,保持旧行为自动追加“旧标题:...”块。两种情况都会追加“请只返回新标题,不要解释。”输出约束;其它 `{...}` 原样保留。 -- `fetch_cmhub_models()` 调 `GET /api/v1/models` 返回别名清单,供⑤设置页动态下拉、⑥AI帮写和⑥正式套图生成的付费前确认共用;Base URL 会先规整为网关根,HTTP 404 映射为 `not_found` 并提示检查 Base URL 或实例是否部署 `/api/v1/models`。GUI 只在进程内按规整网关地址和别名短期缓存公开模型元数据,不缓存 API Key,也不写入配置、SQLite、日志或导出。AI帮写仅在当前 `vision_alias` 命中 `operation_type=vision`、`requires_image=true`、`pricing_status=priced` 且有唯一无条件 `points_cost` 时显示预计扣点;正式套图同样只在当前 `image_alias` 命中唯一无条件的图片生图价格时显示单张和按最终 `specs` 计算的总价。其它情况只提示实际以网关返回为准,预估值不参与扣减或成功判定。 +- `fetch_cmhub_models()` 调 `GET /api/v1/models` 返回别名清单,供设置页动态下拉、商品套图 AI帮写和商品套图正式生成的付费前确认共用;Base URL 会先规整为网关根,HTTP 404 映射为 `not_found` 并提示检查 Base URL 或实例是否部署 `/api/v1/models`。GUI 只在进程内按规整网关地址和别名短期缓存公开模型元数据,不缓存 API Key,也不写入配置、SQLite、日志或导出。AI帮写仅在当前 `vision_alias` 命中 `operation_type=vision`、`requires_image=true`、`pricing_status=priced` 且有唯一无条件 `points_cost` 时显示预计扣点;正式套图同样只在当前 `image_alias` 命中唯一无条件的图片生图价格时显示单张和按最终 `specs` 计算的总价。其它情况只提示实际以网关返回为准,预估值不参与扣减或成功判定。 - `api_type=chat/auto` 走 OpenAI-compatible chat JSON;`api_type=images_edits` 走 multipart form。 -- direct 连接超时参考模型 `connect_timeout_seconds`;**返回超时 = 模型 `timeout_seconds` 或 `appconfig.response_timeout()`(随分辨率:512/1k/2k/4k → 180/240/360/600)**。cmhub 使用 `requests timeout=(connect, read)`,connect 来自 `ai.cmhub.connect_timeout`;cmhub 生文读取等待固定600秒,⑥图片理解读取等待固定120秒且一次读超时不重试,提示结果未确认;②批量生图异步 submit 读取等待36秒、poll 单次读取等待15秒、本地总预算900秒,图片下载读取等待900秒;`gen_cover()` 旧同步兼容路径仍用900秒读取等待。 +- direct 连接超时参考模型 `connect_timeout_seconds`;**返回超时 = 模型 `timeout_seconds` 或 `appconfig.response_timeout()`(随分辨率:512/1k/2k/4k → 180/240/360/600)**。cmhub 使用 `requests timeout=(connect, read)`,connect 来自 `ai.cmhub.connect_timeout`;cmhub 生文读取等待固定600秒,商品套图图片理解读取等待固定120秒且一次读超时不重试,提示结果未确认;②批量生图异步 submit 读取等待36秒、poll 单次读取等待15秒、本地总预算900秒,图片下载读取等待900秒;`gen_cover()` 旧同步兼容路径仍用900秒读取等待。 - 并发数/重试/分辨率/jpg 质量来自 `appconfig.ai_config()`;标题/图片并发会被夹到 1..5,失败重试次数会被夹到 0..10,兼容旧配置中的超限值;Key 本地明文存储,但不入日志、不导出。cmhub 响应的 `points_cost/points_balance/call_id` 不改变返回值,通过 `on_event` metadata 事件上报,GUI 余额/计费展示留给 T-528。 - 标题快、图片慢:分两段、各用各自并发数;失败按 `retry` 重试,仍失败记 error 不阻塞其余。cmhub 批量生图使用内部实际并发 `min(image_concurrency, 5)` 约束 submit+poll 在途数;已有 `tasks.image_task_id` 时直接 GET 续查,不二次 submit;submit 前先持久化 `image_task_key`,submit 成功立即持久化 `image_task_id`;poll `failed/expired` 会清空二者,poll 超时/用户停止/程序退出则保留二者供下次续查。拿到 `image_url` 后交给独立下载/保存线程池,下载线程数与实际生图并发一致且最大 5;T-548 后图片下载后端由 `ai.cmhub.download_with_curl` 控制,默认 `auto`:Windows 且检测到系统 curl 时优先用 curl 下载,否则回退 requests;curl 失败也会自动回退 requests。下载前仍执行公网 URL 校验,curl 使用 `-K` 临时配置文件传入 URL,不把带 token 的 `image_url` 放进命令行参数;`use_system_proxy=false` 时 curl 加 `--noproxy "*"`。下载失败最多安全重试 3 次,只复用同一个 `image_url`,不会重新调用 cmhub 生图接口;下载总耗时超过 20 秒时写“图片下载较慢”警告;direct 兼容路径暂保持原 `image_concurrency` 语义。 - cmhub 图片地址兼容:优先递归读取 `image_url` / `image_url.url` / `images[]` / `image_urls[]` 等字段;若返回 `/generated/...` 或 `generated/...` 相对路径,先按 cmhub Base URL 补全为完整 `http(s)` URL,再进入原有公网 URL 安全校验和下载。 @@ -363,7 +363,7 @@ render_prompt(template_text, task) -> str ## 商品套图模块(`app/product_suite.py` + `app/image_studio*.py`,已建) -商品套图是 ⑥ Tab 的本地图片项目工作区,不自动上传蝦皮、不修改线上商品。底层继续复用旧 AI工场的数据表与生成服务,避免迁移既有项目数据。 +商品套图是主窗口中的本地图片项目工作区,不自动上传蝦皮、不修改线上商品。底层继续复用旧 AI工场的数据表与生成服务,避免迁移既有项目数据。 ```python # app/image_studio.py @@ -424,8 +424,8 @@ export_generation_round(project_id, generation_round_key, parent_dir, path=None, - 项目唯一键为账号别名 + 商品 ID;图片文件默认在 `data/images/pool///` 下分 `originals/generated/exports`,删除生成图进入项目内 `.trash` 并可撤销。 - `image_studio_projects.suite_settings_json` 保存平台/国家/语言/比例/逐图主图/分类数量;`draft_prompt` 保存商品卖点。有效原图上限16张,missing 历史不占名额。 - T-643 后 `image_studio_projects.current_generation_round_key` 记录项目主界面的当前生成轮次;`image_studio_jobs.generation_round_key` 与 `generation_slot_index` 记录整轮归属和稳定展示槽位。`promote_generation_round_if_success()` 只会在同项目、同轮已存在成功 job 时原子切换当前轮;`list_generation_rounds()` 返回轮次摘要,`list_generation_round_current_jobs()` 返回每个槽位最新尝试,`list_generation_round_attempts()` 返回完整尝试历史。旧 job 保持 NULL 并按“旧版历史记录”兼容,不猜测轮次边界。 -- T-646 后 `list_global_generation_rounds()` 是⑥跨项目历史的唯一查询入口:仅返回未软删除项目的轮次摘要,按创建时间倒序并支持账号/店铺、商品 ID、项目 ID、`limit`/`offset` 筛选;正常轮次只统计每个稳定槽位的最新有效 job,完整尝试数仅用于计算重试数,NULL 轮次保留“旧版历史记录”语义。`ProductSuiteGlobalHistoryDialog` 组合该查询和 `list_generation_round_current_jobs()`,默认读取30轮,显示受限缩略图并可按轮预览;窗口不写业务数据,也不改变当前轮次。`ProductSuiteHistoryExportWorker` 在后台调用 `export_generation_round()`,只复制该轮成功且存在的输出 asset 到用户选择目录中新建的安全子目录;同名目录追加稳定序号,不覆盖外部文件,部分文件失败只汇总中文结果,源 asset 保持不变。 -- T-648 后 `get_successful_generation_history_summary()` 是⑥常规新一轮生成前的只读判据:它只查询未软删除当前项目的最新有效成功 job,返回成功轮数、成功图片数和最近成功时间;正常轮次按稳定槽位取最新尝试,NULL 旧版轮次整体按一轮计数,只有失败/取消/进行中的 job 返回零摘要。该 API 不扫描文件目录、不创建 job/asset/轮次,也不改变当前轮;GUI 的“查看历史”仅调用全局历史窗口并临时启用当前商品筛选。 +- T-646 后 `list_global_generation_rounds()` 是商品套图跨项目历史的唯一查询入口:仅返回未软删除项目的轮次摘要,按创建时间倒序并支持账号/店铺、商品 ID、项目 ID、`limit`/`offset` 筛选;正常轮次只统计每个稳定槽位的最新有效 job,完整尝试数仅用于计算重试数,NULL 轮次保留“旧版历史记录”语义。`ProductSuiteGlobalHistoryDialog` 组合该查询和 `list_generation_round_current_jobs()`,默认读取30轮,显示受限缩略图并可按轮预览;窗口不写业务数据,也不改变当前轮次。`ProductSuiteHistoryExportWorker` 在后台调用 `export_generation_round()`,只复制该轮成功且存在的输出 asset 到用户选择目录中新建的安全子目录;同名目录追加稳定序号,不覆盖外部文件,部分文件失败只汇总中文结果,源 asset 保持不变。 +- T-648 后 `get_successful_generation_history_summary()` 是商品套图常规新一轮生成前的只读判据:它只查询未软删除当前项目的最新有效成功 job,返回成功轮数、成功图片数和最近成功时间;正常轮次按稳定槽位取最新尝试,NULL 旧版轮次整体按一轮计数,只有失败/取消/进行中的 job 返回零摘要。该 API 不扫描文件目录、不创建 job/asset/轮次,也不改变当前轮;GUI 的“查看历史”仅调用全局历史窗口并临时启用当前商品筛选。 - T-649 后 `list_global_history_accounts()` 只返回含 job 且未软删除项目的历史账号别名,供历史窗口补齐已删除账号的可选项。`list_global_generation_rounds(account_alias=...)` 以 `projects.account_alias = ?` 精确筛选;传入该参数时优先于旧兼容参数 `account_query`,后者仅保留给既有调用的模糊查询。当前账号显示文本来自账号管理,历史缺失账号只显示明确的历史标识,UI 不通过显示文本反查筛选值。 - 拉取蝦皮原主图只读:复用 `editor.open_product(..., bring_to_front=False)` 和 `editor.read_product_image_urls()`,不上传、不拖拽、不点击更新。 - 原图下载走 `image_studio_images` 的公网 URL、大小、Content-Type、重定向和 PIL 解码校验;只在用户单击时落盘。 @@ -441,55 +441,55 @@ export_generation_round(project_id, generation_round_key, parent_dir, path=None, ```python # GUI 入口 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 GenerateTab(QWidget) # ② AI生成:提示词管理 + 筛选任务 + 生成封面图片成本开关 + 开始/停止生成 + 新旧封面预览 + AI生成运行日志 class ApplyTab(QWidget) # ③ 更新蝦皮:筛选已生成任务 + 检查本轮更新 + 缺失内容校验 + 确认后分批真实更新 + 运行日志 -class SettingsTab(QWidget) # ⑤ 设置:cmhub 网关配置 + 响应式三列布局 + 角色/生成参数/路径端口 + 蝦皮更新安全 + 未保存状态追踪 -class ProductSuiteTab(QWidget) # ⑥ 商品套图:多任务、原图、结构配置、AI帮写、cmhub生成、历史结果 +class SettingsTab(QWidget) # 设置:cmhub 网关配置 + 响应式三列布局 + 角色/生成参数/路径端口 + 蝦皮更新安全 + 未保存状态追踪 +class ProductSuiteTab(QWidget) # 商品套图:多任务、原图、结构配置、AI帮写、cmhub生成、历史结果 class ImageStudioTab(QWidget) # 旧AI工场兼容实现;主窗口不再创建 class CollectWorker(BaseWorker) # ① 后台采集:账号就绪预检 -> editor.collect -> db.set_collected/mark_skipped/mark_failed class GenerateWorker(BaseWorker) # ② 后台生成:ai.generate_batch -> db.set_generated/set_generated_cover/mark_failed + 进度 class ApplyWorker(BaseWorker) # ③ 后台更新:账号就绪预检 -> 检查或按批调用 editor.apply_task(...) -> db.set_applied/mark_skipped class WriteBackWorker(BaseWorker) # ①/③ 后台回写:旧字段或更新结果写回原 Excel -class AIModelTestWorker(BaseWorker) # ⑤ 后台测试 AI 模型连接:appconfig.test_ai_model -class ImageStudioPullImagesWorker(BaseWorker) # ⑥ 后台只读拉蝦皮原主图 URL;安全边界协作停止 -class ImageStudioDownloadOriginalWorker(BaseWorker)# ⑥ 后台下载远程原图 -class ImageStudioGenerateJobsWorker(BaseWorker) # ⑥ 后台提交/查询/下载 cmhub 生图 job -class ImageStudioResumeJobsWorker(BaseWorker) # ⑥ 后台恢复已有 task_id 的生图 job -class ImageStudioExportWorker(BaseWorker) # ⑥ 后台导出终选 JPEG -class ProductSuiteImportImagesWorker(BaseWorker) # ⑥ 后台校验并复制本地/剪贴板商品原图 -class ProductSuiteGenerateWorker(BaseWorker) # ⑥ 按套图job规划提交/查询/下载 -class ProductSuiteAiWriteWorker(BaseWorker) # ⑥ 后台用本地原图调用图片理解,生成商品卖点与画面要求 +class AIModelTestWorker(BaseWorker) # 设置 后台测试 AI 模型连接:appconfig.test_ai_model +class ImageStudioPullImagesWorker(BaseWorker) # 商品套图 后台只读拉蝦皮原主图 URL;安全边界协作停止 +class ImageStudioDownloadOriginalWorker(BaseWorker)# 商品套图 后台下载远程原图 +class ImageStudioGenerateJobsWorker(BaseWorker) # 商品套图 后台提交/查询/下载 cmhub 生图 job +class ImageStudioResumeJobsWorker(BaseWorker) # 商品套图 后台恢复已有 task_id 的生图 job +class ImageStudioExportWorker(BaseWorker) # 商品套图 后台导出终选 JPEG +class ProductSuiteImportImagesWorker(BaseWorker) # 商品套图 后台校验并复制本地/剪贴板商品原图 +class ProductSuiteGenerateWorker(BaseWorker) # 商品套图 按套图job规划提交/查询/下载 +class ProductSuiteAiWriteWorker(BaseWorker) # 商品套图 后台用本地原图调用图片理解,生成商品卖点与画面要求 class CMHubModelCatalogWorker(BaseWorker) # 后台读取公开模型目录并填充进程内缓存,不提交图片、不扣点 -class ProductSuiteHistoryExportWorker(BaseWorker) # ⑥ 后台复制一轮历史成功生成图 +class ProductSuiteHistoryExportWorker(BaseWorker) # 商品套图 后台复制一轮历史成功生成图 class TaskTableModel(QAbstractTableModel) # 任务表格模型:账号/别名/商品ID/阶段;未匹配别名显示“略过” class GenerateTaskTableModel(QAbstractTableModel) # ② 任务表格模型:店铺/商品ID/旧标题/新标题/状态;generated/未提交/非运行中新标题可本地编辑 class ApplyTaskTableModel(QAbstractTableModel) # ③ 任务表格模型:店铺/商品ID/新标题/新封面/阶段/结果;保持只读,重置更新状态走右键菜单 -class AccountsTab(QWidget) # ④ 账号管理:表格 + 增删改 + 启动登录 + 检测登录 + 快捷方式 +class AccountsTab(QWidget) # 账号管理:表格 + 增删改 + 启动登录 + 检测登录 + 快捷方式 class AccountDialog(QDialog) # 账号编辑弹窗;密码 QLineEdit.Password TAB_TITLES: list[str] # 固定 Tab 顺序 TAB_STYLE: str # 顶层 Tab 栏防误点样式:最小宽度/padding/间距/当前态 ``` -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生成布局/提示词/开始生成/停止/封面对照预览、③ 更新蝦皮筛选列表与检查/确认后分批真实更新、④ 账号管理、⑤ cmhub 设置、⑥ 商品套图生成。缺 PySide6 时 `main()` 返回 1 并输出明确提示。 +`MainWindow` 已实现六 Tab、① 导入采集任务列表、② AI生成布局/提示词/开始生成/停止/封面对照预览、③ 更新蝦皮筛选列表与检查/确认后分批真实更新、账号管理、设置中的 cmhub 配置、商品套图生成。缺 PySide6 时 `main()` 返回 1 并输出明确提示。 主 Tab 栏必须在 `MainWindow` 初始化时应用 `TAB_STYLE`:6 个 Tab 不使用 Qt 默认紧凑宽度,需保证点击区域稳定、间距清晰、当前 Tab 高亮明显。该样式属于全局导航基础,不归后续业务 Tab 任务重复实现。 -④ 账号管理要点: +账号管理要点: - 表格列:账号名、别名、地区、端口、登录状态、备注;不展示密码。 - 弹窗字段:账号名、别名、地区、调试端口、密码、备注、slug、数据目录;密码框使用打码显示;首次保存/变更非空密码前弹窗提示“本地明文保存”。 - 「启动登录」必须幂等:新增账号后不自动启动;若该账号 Chrome/CDP 已打开,则复用现有窗口并打开/激活卖家中心登录 tab,不重复启动 Chrome,也不覆盖普通页面;未打开时用卖家中心初始 URL 新启动并复用初始 page target,异常时最多兜底新建一个 tab。启动期间禁用账号选择及增删改/登录相关操作,连续触发只记 `ignored` 日志;成功或失败均恢复控件。`chrome_launch` 日志记录初始 target 是否复用、是否兜底建 tab 和原因。「检测登录」用 worker 跑 `accounts.detect_login()` 并刷新状态列;若当前 URL 跳到 `accounts.shopee.tw/seller/login`,状态必须显示未登录;「快捷方式」调用 `accounts.create_shortcut()` 生成默认桌面 `.lnk`。 -⑤ 设置当前要点(T-501): +设置当前要点(T-501): - `SettingsTab` 使用居中内容区 + 适度左右留白布局,当前留白已从 T-506 初始实现缩短到约 40%;实现上使用最大内容宽度和自适应 margin,避免固定像素导致小屏挤压。各设置组默认响应式 3 列表单:短字段占 1 格,长字段(URL/API Key/路径)跨 2 格或 3 格,窄窗口降为 2 列/1 列。点击「保存设置」成功后,调用 `QMessageBox.information` 弹出“设置已保存”轻量提示框,同时保留状态栏提示。T-531 已完成:`save_app_settings()` 返回 bool,成功写 `data/config.json` + `data/config/cmhub.json` 后清 dirty,失败保留 dirty 并让调用方阻止离开。 - `SettingsTab` 的 cmhub 网关配置:Base URL 保存/刷新前规整为网关根;API Key 单独读写 `data/config/cmhub.json`;别名下拉来自 `fetch_cmhub_models()`,按 `operation_type` 分生文/生图/图片理解并过滤未计价别名,其中图片理解额外要求 `requires_image=true`;显示托管档位、展示名、扣点和需参考图提示。已保存但当前模型列表不可用的图片理解别名必须保留并标明暂不可用;「测试连接/查余额」调用 cmhub models + balance helper;保存设置固定写 `ai.backend=cmhub`。`refresh_cmhub_models()` / `test_cmhub_connection()` 使用输入框实时值但不得自动保存,成功文案提醒用户保存。`is_dirty()` / `discard_unsaved_changes()` / `_suspend_dirty`(或等价机制)用于 T-531:用户编辑置脏,程序化回填不置脏,放弃时重新加载 `data/config.json` + `data/config/cmhub.json` 并回填控件。 - T-532 要求 `SettingsTab._on_cmhub_finished()` 从 worker payload 的 `balance` / user/account 字段提取 cmhub 账号身份,成功文案优先显示 `cmhub 账号「<账号名>」连接成功:...`;当前 `/balance` 结构兼容 `{ "user": "cmhub_user", "points_balance": 88, "account": { "username": "cmhub_user", "display_name": "主账号" } }`,显示名优先 `account.display_name`,再兜底 `account.username` / `user` / 顶层常见字段;账号字段缺失时保持 `cmhub 连接成功:...`。显示名必须脱敏处理邮箱,且不得把 API Key、token 或完整敏感响应写入 GUI、run log 或诊断日志。 -- `MainWindow` 已负责⑤设置页离开守卫:切 Tab 与 `closeEvent` 发现 `SettingsTab.is_dirty()` 时弹保存/放弃/取消;保存成功后继续,保存失败或取消时回到⑤。由于 `QTabWidget.currentChanged` 是切换后信号,需维护上一个 index,并用 `_reverting_tab_change` 或等价 guard 防止 `setCurrentIndex()` 递归。 +- `MainWindow` 已负责设置页离开守卫:切 Tab 与 `closeEvent` 发现 `SettingsTab.is_dirty()` 时弹保存/放弃/取消;保存成功后继续,保存失败或取消时回到设置。由于 `QTabWidget.currentChanged` 是切换后信号,需维护上一个 index,并用 `_reverting_tab_change` 或等价 guard 防止 `setCurrentIndex()` 递归。 - 模型详情字段按 3 个组件一组排列:启用、类别、api_type、连接超时等短字段一格;服务商名、模型 ID 视宽度占一格或两格;网址、密钥跨整行或跨 2/3 列。 - 模型数据读写复用 `appconfig.list_ai_models(..., reveal_api_key=True)`、`add_ai_model()`、`update_ai_model()`、`delete_ai_model()`;保存时保留现有 `extra_body` 与 `timeout_seconds`。 - 密钥字段使用 `QLineEdit.Password` 打码显示;首次保存/变更非空 Key 前弹窗提示“本地明文保存”;明文只写入已 gitignore 的 `data/config/ai_models.json`,不得进入日志/导出。 @@ -509,14 +509,14 @@ T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负 - 任务列表使用 `QTableView + TaskTableModel`,列为:账号、别名、商品ID、阶段。 - 账号列优先显示匹配到的 `accounts.account_name`;未匹配账号时保留 Excel 输入账号名。 - 别名未匹配 `accounts.alias` 时列表阶段列显示“略过”;点击「采集旧标题/旧封面」后由 `CollectWorker` 逐条写库为 `skipped`,原因 `别名未匹配账号`。 -- 「采集旧标题/旧封面」通过 `CollectWorker` 后台执行,只处理 `stage=imported` 的任务;采集前先做账号就绪预检。无账号、当前批次匹配账号未启动 CDP 端口或未登录时,返回 `blocked=True`,GUI 弹窗汇总并跳转/引导去④账号管理,不进入逐条采集、不写 skipped/failed。预检通过后,已匹配任务调用 `editor.collect()` 下载旧封面到 `image_dir///__old.jpg` 并 `db.set_collected()`;别名未匹配任务仍逐条 `mark_skipped`;单条失败 `mark_failed(..., "collect", error)` 后继续。采集完成且本轮成功采集数量大于 0 时,自动触发当前批次旧字段回写;锁文件失败时只提示,不回滚 SQLite。 +- 「采集旧标题/旧封面」通过 `CollectWorker` 后台执行,只处理 `stage=imported` 的任务;采集前先做账号就绪预检。无账号、当前批次匹配账号未启动 CDP 端口或未登录时,返回 `blocked=True`,GUI 弹窗汇总并跳转/引导去账号管理,不进入逐条采集、不写 skipped/failed。预检通过后,已匹配任务调用 `editor.collect()` 下载旧封面到 `image_dir///__old.jpg` 并 `db.set_collected()`;别名未匹配任务仍逐条 `mark_skipped`;单条失败 `mark_failed(..., "collect", error)` 后继续。采集完成且本轮成功采集数量大于 0 时,自动触发当前批次旧字段回写;锁文件失败时只提示,不回滚 SQLite。 - 采集打开商品页时,若本轮自动新建 tab,采集完成后会关闭该 tab,并在最多 2 秒内确认 target 已从 `/json` 消失;确认超时只写 warning/诊断,不把采集成功改成失败。若失败发生在 `open_product()` 内部且尚未返回 `cdp`,也要关闭本轮自动新建 tab;若复用用户已打开的商品页,只断开 CDP 连接不关闭页面。 - 采集中途登录检测必须快速跳过正在销毁的旧商品 target,改连其他有效 Shopee 页面。Cookie API 调用失败返回 `LOGIN_CHECK_TARGET_UNAVAILABLE`,只有 Cookie API 成功返回空会话时才返回 `NO_SESSION_COOKIE`;两者都不按明确掉登录批量略过,显式 `LOGIN_PAGE` 仍按账号需登录处理。retry/recovered 运行日志包含当前任务 ID 和商品 ID,避免与上一条采集成功日志混淆。 - 采集打开商品页失败时,`CollectWorker` 应把 `open_product()` 捕获到的 Shopee toast 文案写入 `run_log_events` 和 `tasks.last_error`;商品 ID 失效、无权限、店铺不匹配等场景不得只显示泛化超时。① `TaskTableModel` 的“阶段”列只在 `last_error` 明确为商品失效类错误时显示“商品失效”,否则仍按 `status=failed` 显示“失败”;底层不新增 `stage` 枚举。 - 「停止」调用 worker 的协作式 `cancel()`,已开始的单条跑到安全边界后结束。 - 「回写旧数据到 Excel」通过 `WriteBackWorker` 后台调用 `excel.write_back()`,把已采集旧标题/旧封面路径按原 Excel 行定位写回;该按钮主要作为自动回写失败后的手动重试入口。原文件被占用时弹窗提示关闭后重试,SQLite 采集结果不回滚。 -- 采集前由 `CollectWorker` 做账号就绪预检:无账号、当前批次匹配账号未启动 CDP 端口或未登录时,返回 `blocked=True`,GUI 弹窗汇总并跳转/引导去④账号管理;不无提示批量启动所有账号 Chrome。匹配账号未登录属于预检阻断,不是逐条 skipped。 +- 采集前由 `CollectWorker` 做账号就绪预检:无账号、当前批次匹配账号未启动 CDP 端口或未登录时,返回 `blocked=True`,GUI 弹窗汇总并跳转/引导去账号管理;不无提示批量启动所有账号 Chrome。匹配账号未登录属于预检阻断,不是逐条 skipped。 ② AI生成当前要点(T-302/T-302p/T-303/T-303b + 诊断日志补丁): @@ -544,7 +544,7 @@ T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负 - 用户点否/取消时不执行、不改库;用户点是后才创建 `ApplyWorker` 做真实提交。 - `ApplyWorker` 只处理当前筛选结果里 `stage=generated`、状态为 `success/pending/failed`,且满足当前 `update_mode` 所需内容的任务;只更新标题时不替换封面,只更新封面时不改标题。已更新和略过记录仅查看,不会再次提交,除非用户先用 T-404a 的「重置更新状态」把选中记录退回可更新。 - 检查本轮更新:不做账号登录预检,不调用 `editor.apply_task()`,不写任务状态,不回写 Excel;只把每条“将更新/将略过”写入运行日志并弹汇总。 -- 真实更新前先做账号就绪预检:无账号、当前筛选结果匹配账号 Chrome 未启动、CDP 端口不可访问、未登录,或本轮涉及账号调试端口冲突时,返回 `blocked=True`,GUI 弹窗汇总并跳转/引导去④账号管理;预检不通过时不调用 `editor.apply_task()`、不写失败状态、不自动启动 Chrome。 +- 真实更新前先做账号就绪预检:无账号、当前筛选结果匹配账号 Chrome 未启动、CDP 端口不可访问、未登录,或本轮涉及账号调试端口冲突时,返回 `blocked=True`,GUI 弹窗汇总并跳转/引导去账号管理;预检不通过时不调用 `editor.apply_task()`、不写失败状态、不自动启动 Chrome。 - 预检通过后默认串行;若 `max_parallel_accounts>1`,按账号分组并行执行,不同账号可同时跑,同一账号内仍串行。每条执行 `db.mark_running(..., "apply")` → `editor.apply_task(account, task)` → `db.set_applied()`;成功推进 `stage=applied/status=success/committed=1`,失败保持原 stage、`status=failed/committed=0/last_error`,单条失败继续下一条。 - 真实更新打开商品页失败时,`ApplyWorker` 应把 `open_product()` 捕获到的 Shopee toast 文案上浮到③可见运行日志、`run_log_events` 和任务失败原因;若失败发生在 `open_product()` 内部,本轮自动新建 tab 要关闭,复用用户已有 tab 不关闭;本地 `data/logs/` 可保存失败现场 HTML/toast JSON 片段供开发排查,但必须脱敏。 @@ -588,7 +588,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(...)`,逐条 `set_applied()`,失败继续;账号未就绪或端口冲突时整体阻断并引导④,不进入逐条提交,也不静默启动账号 Chrome。点击停止为协作式停止:当前商品完成后不再开始新商品或下一批。 +- `ApplyWorker` 支持检查、默认串行和按账号并行;真实更新按 `max_items_per_run` 分批调用 `editor.apply_task(...)`,逐条 `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/routes.md b/docs/routes.md index 04b4e57..570184f 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -1,11 +1,11 @@ # 界面与流程结构 -> 桌面工具,无前端路由。当前正式界面用 **6 Tab GUI(PySide6 `QMainWindow` + `QTabWidget`)+ 流水线** 约定界面职责与导航;⑥商品套图已替换旧 AI工场入口。 +> 桌面工具,无前端路由。当前正式界面用 **6 Tab GUI(PySide6 `QMainWindow` + `QTabWidget`)+ 流水线** 约定界面职责与导航;商品套图已替换旧 AI工场入口。 ## Tab 顺序与职责(工作流优先) ``` -① 导入采集 │ ② AI生成 │ ③ 更新蝦皮 │ ④ 账号管理 │ ⑤ 设置 │ ⑥ 商品套图 +① 导入采集 │ ② AI生成 │ ③ 更新蝦皮 │ 账号管理 │ 设置 │ 商品套图 ``` | Tab | 职责 | 风险 | @@ -13,9 +13,9 @@ | ① 导入采集 | 导入多个 Excel;任务列表;**采集**商品当前的旧标题/旧封面(只读),封面图下载本地;回写 Excel 旧字段 | 只读,低 | | ② AI生成 | 左侧标题/封面**提示词**;右侧按批次/店铺/商品ID/状态筛选任务列表;AI 生成新标题,并按本轮开关可选生成新封面;表格拆分显示「标题状态 / 图片状态」;已生成任务可本地微调新标题;双击看新旧封面 | 不触线上,中 | | ③ 更新蝦皮 | 对**已生成**任务点击「开始更新」后弹窗确认;确认后打开编辑页换标题+封面并逐条点「更新」提交;结果回写 Excel | **写线上,高** | -| ④ 账号管理 | Shopee 账号(账号名/别名/数据目录/端口/密码本地明文仅参考/登录状态);启动登录、检测登录、生成快捷方式;新增账号不自动启动 Chrome,首次启动复用初始卖家中心页,重复操作复用已打开的同账号 Chrome,避免重复开窗口;检测登录遇到 `accounts.shopee.tw/seller/login` 必须显示未登录 | 中 | -| ⑤ 设置 | cmhub 网关/API Key、生文/生图/图片理解别名、托管档位提示、生成参数、Chrome 路径、默认端口、蝦皮更新执行参数等 | — | -| ⑥ 商品套图 | 按账号+商品ID管理本地图片项目;拉取/添加商品原图,AI帮写理解图片内容,按套图分类异步生图,查看历史与重试 | 本地生成,中 | +| 账号管理 | Shopee 账号(账号名/别名/数据目录/端口/密码本地明文仅参考/登录状态);启动登录、检测登录、生成快捷方式;新增账号不自动启动 Chrome,首次启动复用初始卖家中心页,重复操作复用已打开的同账号 Chrome,避免重复开窗口;检测登录遇到 `accounts.shopee.tw/seller/login` 必须显示未登录 | 中 | +| 设置 | cmhub 网关/API Key、生文/生图/图片理解别名、托管档位提示、生成参数、Chrome 路径、默认端口、蝦皮更新执行参数等 | — | +| 商品套图 | 按账号+商品ID管理本地图片项目;拉取/添加商品原图,AI帮写理解图片内容,按套图分类异步生图,查看历史与重试 | 本地生成,中 | 旧 `ImageStudioTab` 与 `image_studio_*` SQLite/图片资产服务继续保留作内部兼容;主窗口只创建 `ProductSuiteTab`,不会并列暴露两套第六 Tab,也不会删除或迁移用户既有项目数据。 @@ -48,10 +48,10 @@ ## 首次使用引导保护 -- ① 导入采集 与 ③ 更新蝦皮 都依赖**账号已配置且已登录**(在 ④ 账号管理)。 -- ① 点击「采集旧标题/旧封面」后,会为本轮匹配到账号的店铺自动确保 Chrome 就绪:已打开则复用,未打开才启动;随后逐账号检测蝦皮登录态。明确进入登录页的账号任务整组略过并在结束汇总中提示去④人工登录;`NO_SESSION_COOKIE` 或 CDP 短暂读取异常只视为登录状态暂不确定,重试后仍不确定也继续尝试采集,避免误判批量略过。 +- ① 导入采集 与 ③ 更新蝦皮 都依赖**账号已配置且已登录**(在 账号管理)。 +- ① 点击「采集旧标题/旧封面」后,会为本轮匹配到账号的店铺自动确保 Chrome 就绪:已打开则复用,未打开才启动;随后逐账号检测蝦皮登录态。明确进入登录页的账号任务整组略过并在结束汇总中提示去账号管理人工登录;`NO_SESSION_COOKIE` 或 CDP 短暂读取异常只视为登录状态暂不确定,重试后仍不确定也继续尝试采集,避免误判批量略过。 - ③ 点击「开始更新」后必须检查当前筛选结果涉及的账号;只要有账号 Chrome 未启动、CDP 端口不可访问或 Shopee 未登录,就弹窗列出账号并中止本轮更新,不创建真实更新 worker,不提交任何商品。 -- 可以提供「打开账号管理」或「启动登录」入口辅助用户处理当前账号;③ 不要无提示批量启动所有账号 Chrome,避免在提交线上前开错账号或启动过多浏览器进程。用户主动点击④「启动登录」时必须先检查该账号 CDP 端口:已打开则复用现有 Chrome 并打开/激活卖家中心 tab,不重复 `Popen`;未打开则直接携带卖家中心 URL 冷启动并复用初始 page target,不先开空白页再额外创建页面。启动过程中账号列表和相关按钮禁用,连续点击不排队启动第二轮。 +- 可以提供「打开账号管理」或「启动登录」入口辅助用户处理当前账号;③ 不要无提示批量启动所有账号 Chrome,避免在提交线上前开错账号或启动过多浏览器进程。用户主动点击账号管理中的「启动登录」时必须先检查该账号 CDP 端口:已打开则复用现有 Chrome 并打开/激活卖家中心 tab,不重复 `Popen`;未打开则直接携带卖家中心 URL 冷启动并复用初始 page target,不先开空白页再额外创建页面。启动过程中账号列表和相关按钮禁用,连续点击不排队启动第二轮。 - 老用户账号已就绪则无感。 ## ① 导入采集 @@ -107,7 +107,7 @@ - 筛选行提供「打开图片文件夹」按钮,用于只读打开本地图片目录:选中某行时打开该商品所在账号图片文件夹(优先打开已有新/旧封面文件的真实父目录,缺失时回退到规范账号目录);未选行且选择具体批次时打开该批次图片文件夹;未选行且为全部批次时打开图片根目录。目录不存在只中文提示,不自动创建目录,不修改任务状态。 - 右下:任务列表(店铺名、商品id、旧标题、新标题、标题状态、图片状态)+ AI生成运行日志;标题/图片状态由 `new_title`、`new_cover_path`、`stage/status` 和失败步骤推导,帮助用户区分“标题未生成 / 图片未生成 / 标题成功但图片失败”。商品ID列按原等分宽度约 50% 显示;标题状态和图片状态列在 T-554 基础上再缩到约 33%,缩出的宽度平均给旧标题和新标题。已生成、未提交线上、非运行中的任务可双击「新标题」列本地微调,写回 `tasks.new_title`,清空 `last_error` 并回到可更新;双击其他列弹窗展示旧封面、新封面和历史候选图。 - 底部**单个「开始生成」+「停止」**,并增加「生成内容」下拉:默认只生成标题,可选只生成封面或生成标题和封面;只生成封面不调用生文,有新标题时优先使用,没有时用已采集旧标题作为封面prompt参考,新旧标题都为空才不纳入。标题/图片两条进度条右侧分别显示同宽用时标签(`生标题用时 N 秒` / `生图用时 N 秒`),运行中每秒递增,完成/停止后冻结;原图片进度条右侧的失败数和 cmhub 余额不再占用该位置。cmhub 模式会把用户设置的图片并发内部限制到最大 5,并用同样最大 5 的独立下载线程池拉取 `image_url`,不新增用户可见下载并发配置;运行日志显示用户设置并发和实际并发。下拉状态持久化到 `config.json` 的 `ai.generate_mode`,并继续写回旧兼容 `ai.generate_cover`。 -- 生成参数(标题/图片并发数、失败重试、分辨率、jpg 质量、cmhub 网关/Key/别名)在 **⑤ 设置**;②只暴露本轮生成标题/封面/图文的内容模式。⑤ 不新增“下载并发”控件;cmhub 图片下载并发由程序按实际生图并发自动计算,最大 5。 +- 生成参数(标题/图片并发数、失败重试、分辨率、jpg 质量、cmhub 网关/Key/别名)在 **设置**;②只暴露本轮生成标题/封面/图文的内容模式。设置不新增“下载并发”控件;cmhub 图片下载并发由程序按实际生图并发自动计算,最大 5。 - 只生成标题时标题成功即写库并进入 `generated`,保留已有封面;只生成封面时通过组件级写库只保存 `new_cover_path`,不覆盖已有标题,也不把旧标题写入空的 `new_title`;生成标题和封面时按缺失组件增量补齐。“有新封面、无新标题”时标题状态为待生成、图片状态为已生成,后续补标题不重复生图。三种模式都写 `run_type=generate` 的 `run_logs/run_log_events` 和用户可读滚动日志;日志开头明确显示本轮生成内容。点击「开始生成」时先清空②界面可见日志并写入本轮开始摘要,运行中只追加本轮日志;不删除历史 `run_logs/run_log_events` 或本地 `data/logs/`。进入页面默认可显示“本轮日志会在开始运行后显示”,历史日志不自动混入当前运行界面。「停止」取消未开始项,可再次「开始生成」对剩余继续。 - 「重置生成结果」支持选中任务或当前筛选结果,运行中禁用;确认框提供「重置标题 / 重置封面 / 重置全部」,只改本地 DB,默认不删除本地新封面文件。若范围内包含已提交线上记录,必须提示本地重置不回滚蝦皮,重生成后再更新会再次提交线上。 - **无逐条人工审核环节**;新标题默认使用 AI 输出,但允许对已生成且未提交线上的单行做本地微调。封面画廊内「重置图片」只清当前任务 `new_cover_path` 并归档旧图,不启动单条生图;用户翻看完后用状态筛选「待生成」并点击「开始生成」批量补封面。生成完即可进入 ③,③ 开始更新前会做批量确认。 @@ -131,7 +131,7 @@ - 状态筛选:`已生成` 只跑未更新的;`失败` 用于**失败重试**;`已更新成功/略过` 仅查看。 - 「更新内容」下拉支持只更新标题、只更新封面、更新标题和封面;点击「检查本轮更新」或「开始更新」前先按当前模式检查 `new_title` / `new_cover_path`,缺失时中文弹窗阻断,不打开 Chrome、不改任务状态。 - 「检查本轮更新」只读取当前筛选结果并写运行日志,不打开 Shopee、不提交、不改任务状态;弹窗/日志展示总任务数、店铺分布、当前更新内容、会更新标题/封面、略过原因、每批最大条数和预计批次数。 -- 缺失内容校验通过后,点击「开始更新」读取 ⑤ `shopee_update` 执行设置:普通正式更新不再以测试商品 ID 或旧真实提交开关限制当前筛选结果,允许当前筛选结果包含多个真实商品 ID;`max_items_per_run` 作为**每批最大更新条数**,当前筛选总数超过该值时不阻断,而是自动分批执行。 +- 缺失内容校验通过后,点击「开始更新」读取设置中的 `shopee_update` 执行设置:普通正式更新不再以测试商品 ID 或旧真实提交开关限制当前筛选结果,允许当前筛选结果包含多个真实商品 ID;`max_items_per_run` 作为**每批最大更新条数**,当前筛选总数超过该值时不阻断,而是自动分批执行。 - 弹窗展示本次筛选条件、更新内容、任务总数、每批最大条数、预计批次数、执行设置和“将提交线上”的风险提示;用户点「是/确认」才开始,点「否/取消」不执行。 - 真实更新第一条商品前做账号就绪预检:按当前筛选结果汇总需要的账号;无账号、Chrome 未启动、CDP 端口不可访问、未登录或端口冲突时,弹窗列出具体账号/原因并中止本轮,不自动调用「启动登录」或静默打开 Chrome。 - 对确认后的**已生成(generated)任务**按当前更新内容执行:打开编辑页换标题和/或换封面 → 点页面「更新」 → 如 Shopee 弹出“確定您要更新商品嗎?”确认框(`.eds-modal__content` / `.eds-modal__box`),则只点弹窗主按钮「更新」提交,不点「立即優化」。 @@ -139,18 +139,18 @@ - 打开编辑页失败时,如果 Shopee 弹出错误 toast(如商品 ID 不正确、商品不存在、无权限),③运行日志和任务失败原因必须显示该 toast 文案;同时把 toast HTML/URL/时间写入本地诊断日志。用户不需要手动复制瞬时 toast 的 HTML。 - 更新封面时统一按替换第一张执行:删除第一张前必须已有该任务的本地旧封面备份(①采集得到的 `old_cover_path` 且文件存在);备份缺失时阻断该条更新并提示先采集/修复备份,不盲删线上图片。 -- 默认串行、单条失败继续;⑤「同时更新蝦皮账号」设为 1 时逐个账号执行,设为 2..5 时不同账号同时执行,同一账号内仍串行;真实更新前若本轮账号调试端口冲突则阻断。 +- 默认串行、单条失败继续;设置中的「同时更新蝦皮账号」设为 1 时逐个账号执行,设为 2..5 时不同账号同时执行,同一账号内仍串行;真实更新前若本轮账号调试端口冲突则阻断。 - 「检查本轮更新」只写运行日志与检查汇总,不打开 Shopee、不调用 `editor.apply_task()`、不写任务状态、不回写 Excel。 - 真实更新每条立即写回 SQLite(committed/状态/error),失败不阻塞后续任务;检查和真实更新都会写 `run_logs/run_log_events`。点击「检查本轮更新」或「开始更新」时先清空③界面可见日志并写入本轮检查/更新开始摘要,运行中只追加本轮日志;不删除历史 `run_logs/run_log_events` 或本地 `data/logs/`,历史日志不自动混入当前运行界面。 - 分批更新时「停止」为协作式停止:已开始的当前商品跑到安全边界并写库后停止,不再开始新商品、不进入下一批;未开始任务保持原状态,下次可继续。 -- 更新前做账号就绪预检:无账号、对应账号 Chrome 未启动、CDP 端口不可访问或未登录时整体阻断并引导去④账号管理,不进入逐条提交,也不自动打开账号 Chrome。 -- 若 ⑤ 开启“成功后关闭本次新开编辑页”,则仅关闭本轮程序自动新开且成功提交的商品页;确认后跳回我的商品列表页时,关闭前等待 2 秒;已进入编辑页后的失败任务和用户原本打开的 tab 不关闭。但如果失败发生在 `open_product()` 内部,程序自动新开的无效商品页 tab 要关闭,避免 Chrome 残留空错误页。 +- 更新前做账号就绪预检:无账号、对应账号 Chrome 未启动、CDP 端口不可访问或未登录时整体阻断并引导去账号管理,不进入逐条提交,也不自动打开账号 Chrome。 +- 若设置中开启“成功后关闭本次新开编辑页”,则仅关闭本轮程序自动新开且成功提交的商品页;确认后跳回我的商品列表页时,关闭前等待 2 秒;已进入编辑页后的失败任务和用户原本打开的 tab 不关闭。但如果失败发生在 `open_product()` 内部,程序自动新开的无效商品页 tab 要关闭,避免 Chrome 残留空错误页。 - 「开始更新」是③的主操作按钮,视觉上必须强于其他批处理按钮。 - 「重置更新状态」从底部批处理按钮移到任务表右键菜单/高级入口,仅作用当前选中单条,运行中禁用;保留 `new_title/new_cover_path`,只把本地状态退回可更新,用于重复测试上传/提交。若 `committed=1`,确认框必须提示线上已提交过、本地重置不回滚蝦皮、重复更新会再次提交;不得静默清除 committed 历史事实。 - 更新完成后自动回写原 Excel:写入新标题、新封面图片路径、更新状态;原文件被锁时提示关闭后点击「回写结果到 Excel」手动重试。 - 自动回写完成后弹窗汇总成功/失败/略过数量与 Excel 回写文件/行数。 -## ④ 账号管理 +## 账号管理 ``` ┌ 账号管理 ─────────────────────────────────────────────────────┐ @@ -162,25 +162,25 @@ 账号弹窗字段:账号名、别名(唯一,Excel 用它匹配)、地区域名、调试端口、密码(本地明文仅参考,保存/变更时提示,UI 打码,不自动登录)、备注;配置目录按别名生成 slug 只读显示。 -## ⑤ 设置 +## 设置 - 设置页整体布局:内容区居中,左右留白已从 T-506 初始实现缩短到约 40%;实现上用最大内容宽度 + 自适应 margin,而不是写死窗口像素。所有设置组默认响应式 3 列表单:短字段占 1 格,URL/API Key/路径等长字段跨 2 格或 3 格;窄窗口自动降为 2 列/1 列。点击「保存设置」成功后,状态栏显示“设置已保存”,并弹出轻量提示框。T-531 已完成:保存成功会清除未保存标记;保存失败时保留未保存标记并阻止离开。 -- cmhub 网关配置(T-529 后普通用户唯一 AI 入口):⑤设置页不再显示「AI 后端」label 或 direct/cmhub 下拉,直接展示 cmhub 网关 Base URL、API Key、生文别名、生图别名、连接超时、刷新别名、测试连接/查余额;Base URL 输入框旁提示“只填网关根,如 https://host”,保存/刷新前会规整掉 `/api`、`/api/v1` 或其它路径。刷新别名/测试连接使用当前输入框内容发起请求,但不自动保存 URL/API Key,成功文案需提醒“记得点保存设置持久化”。 +- cmhub 网关配置(T-529 后普通用户唯一 AI 入口):设置页不再显示「AI 后端」label 或 direct/cmhub 下拉,直接展示 cmhub 网关 Base URL、API Key、生文别名、生图别名、连接超时、刷新别名、测试连接/查余额;Base URL 输入框旁提示“只填网关根,如 https://host”,保存/刷新前会规整掉 `/api`、`/api/v1` 或其它路径。刷新别名/测试连接使用当前输入框内容发起请求,但不自动保存 URL/API Key,成功文案需提醒“记得点保存设置持久化”。 - T-532 需求:测试连接/查余额成功时,如果 cmhub `/balance` 返回账号名、用户名或邮箱等可识别信息,成功提示应显示 `cmhub 账号「<账号名>」连接成功:...`,让用户确认当前 API Key 属于哪个 cmhub 账号;当前接口结构兼容 `{ "user": "cmhub_user", "points_balance": 88, "account": { "username": "cmhub_user", "display_name": "主账号" } }`,优先显示 `account.display_name`,没有时再用 `account.username` / `user` 等兜底;邮箱需脱敏,没有账号信息时保留 `cmhub 连接成功:...` 兜底文案。 - API Key 存在 `data/config/cmhub.json`,本地明文保存;保存/变更时提示;UI 使用密码框打码显示,不进入日志/导出。 - 生文/生图别名来自 `GET /api/v1/models` 动态下拉,过滤未定价别名并展示“默认档 / 高质量档 / 省点档”、展示名、单价/需原图提示;网关临时不可达时保留已存别名。 - - 保存设置固定写 `ai.backend=cmhub`。允许先保存不完整 cmhub 配置,②真正生成时如果缺 Base URL/API Key/别名,会提示去⑤补配置,不静默回退 direct。 - - T-531 已完成:⑤设置页任意可编辑控件变更都进入未保存状态,保存按钮旁显示“● 未保存更改”;切换到其它 Tab 或关闭窗口时弹出保存/放弃/取消。放弃会重新从本地配置文件回填控件,避免未保存的 URL/API Key 留在界面上;程序化回填、保存后重载和刷新别名填充下拉不会误触发未保存状态。 + - 保存设置固定写 `ai.backend=cmhub`。允许先保存不完整 cmhub 配置,②真正生成时如果缺 Base URL/API Key/别名,会提示去设置补配置,不静默回退 direct。 + - T-531 已完成:设置页任意可编辑控件变更都进入未保存状态,保存按钮旁显示“● 未保存更改”;切换到其它 Tab 或关闭窗口时弹出保存/放弃/取消。放弃会重新从本地配置文件回填控件,避免未保存的 URL/API Key 留在界面上;程序化回填、保存后重载和刷新别名填充下拉不会误触发未保存状态。 - direct 模型清单和 `data/config/ai_models.json` 代码路径保留为内部兼容/手工回滚,不在普通 UI 暴露。 - AI 生成参数:标题并发、图片并发、失败重试、分辨率、返回超时等短字段按三列排列;标题/图片并发可选 1..5,失败重试可选 0..10,旧配置超限值会自动夹紧;图片保存质量保留内部默认 90,不在普通 UI 展示。 - - 分辨率为 `512 / 1k / 2k / 4k`,在 cmhub 默认模式下只控制生成图片尺寸;⑤「返回超时」只读展示当前实际等待口径:标题 600 秒、图片 900 秒,不再随分辨率切换显示 180/240/360/600,避免用户误解生图等待时间。 + - 分辨率为 `512 / 1k / 2k / 4k`,在 cmhub 默认模式下只控制生成图片尺寸;设置中的「返回超时」只读展示当前实际等待口径:标题 600 秒、图片 900 秒,不再随分辨率切换显示 180/240/360/600,避免用户误解生图等待时间。 - 保存写入 `config.json` 的 `ai` 段,供 ② AI生成复用;标题/图片模型角色下拉随 direct UI 一起隐藏。 - 路径与端口(T-501b/T-506/T-539/T-580 已接入):组件组改为 3 个组件一组;普通设置页只显示 Chrome 路径、默认端口、端口起止、Chrome 就绪超时。T-538 后账号数据根目录、图片目录、DB 路径固定解析到 `data/` 下,普通 UI 不再提供输入框,避免用户误改后数据分裂;`config.json` 中 `user_data_root` / `image_dir` / `db_path` 字段继续作为内部兼容字段保留,手工配置值仍会被读取和保存。 - 蝦皮更新执行(T-580 已接入):组件组改为 3 个组件一组;普通设置页只保留「每批最大更新条数」和「同时更新蝦皮账号(1..5)」两个执行参数。`1` 表示逐个账号串行,`2..5` 表示按账号分组并行;“dry-run”不作为用户可见开关,改到③成为「检查本轮更新」按钮;测试商品 ID 和旧封面开关仅作为历史/调试兼容字段读取,普通设置页无入口,保存后不再写回。 - ③「更新内容」默认只更新标题,每批最大更新条数默认 1,同时更新蝦皮账号默认 1。 - ③ 点击「开始更新」会先按「更新内容」校验缺失内容,再弹确认框。 -## ⑥ 商品套图 +## 商品套图 ``` ┌ 套图任务1 │ 套图任务2 │ + ───────────────────────────────────┐ @@ -209,26 +209,26 @@ - 生成按钮按当前总数显示并在运行时切换为「停止生成」;确认停止后显示「正在停止...」,重复点击不再弹确认框。每轮生成用独立运行标识隔离旧信号,本轮全部 job 终态或线程结束时都会统一恢复按钮;最终 worker 信号缺失时由数据库终态看门狗兜底,不要求用户重启。停止会取消未开始任务,已提交任务停止本地等待并保留后续继续查询语义;客户端不承诺取消服务端任务或退回点数。T-643 后项目持久化当前生成轮次:常规新轮至少成功一张才替换主结果区,全部失败/取消保留上一当前轮;主结果按稳定槽位显示同轮最新 job,单张重试留在原槽位。旧版无轮次 job 临时显示为“旧版历史记录”,不按时间或图片数量猜测归属。成功图可预览、复制路径、打开目录、重新生成、移入项目废纸篓并撤销,失败卡显示脱敏中文摘要与重试入口。 - T-648 后,常规「生成套图」在原图、卖点和数量校验通过后、费用确认前,若 SQLite 记录显示当前商品已有成功套图,会出现「已有套图生成记录」确认框:用户可查看仅当前商品的全局历史、继续生成新一轮或取消,默认取消;查看历史和取消都不提交任务,继续仍须通过原费用确认后才创建新轮次。失败图片重试、恢复未完成任务和仅失败/已取消历史不出现该确认。 - T-646 后「历史生成」打开全局非模态「套图历史生成记录」窗口,默认显示所有未删除商品项目最近创建的生成轮次,主结果区不切换。T-649 将店铺筛选固定为首项「全部店铺」的下拉:当前账号显示「账号名(账号别名)」,已删除但仍有历史项目的账号显示「历史店铺:别名(账号已删除)」,选择值使用 `account_alias` 精确查询;商品 ID 关键字和“仅当前商品”可与其叠加,默认不限制当前任务。每一行就是一次正常生成轮次,单张失败重试仍归入原行。行内固定显示时间、店铺/账号、商品 ID、成功/失败/停止/重试统计、最多5张缩略图及余量 `+N`,当前轮标记“当前”,NULL 轮次标记“旧版历史记录”,临时项目显示“临时草稿”。双击缩略图或整行从对应图片打开该轮所有可用图的自适应原尺寸浏览;“导出本轮”后台复制该轮成功且本地存在的图片到用户选择目录下的新安全子目录,不覆盖或修改内部图片。旧版记录、全失败轮和本地文件缺失项保留中文说明;不提供批量导出、删除、重试、切换当前轮或再次生成。重复点击复用同一窗口;关闭任务页不关闭全局窗口,应用退出时正常释放。 -- AI帮写和生图按任务独立运行。AI帮写只使用⑤设置的「图片理解别名」调用图片理解能力,不走②标题生成;按商品原图 `source_order` 取1至8张已下载的本地图片,在一次请求中作为同商品的多角度/细节/包装/场景证据集联合理解,超过8张时状态提示只使用前8张,原图勾选不改变输入图片。返回一份可直接编辑的商品级卖点与套图画面要求,按商品概述、可确认卖点、人群与场景、套图画面要求、待确认或避免编造的信息组织,不按图1、图2逐图说明;图片有可见差异时明确为待确认项。单图超过10MiB、总计超过32MiB、没有可用本地图、别名未配置或服务异常时不改现有卖点;图片理解读超时或网络中断提示“结果未确认,请先查看点数余额或稍后重试”,不自动重发。成功状态显示理解图片张数、扣点和余额;AI帮写期间若用户改过卖点,返回后必须确认才覆盖;全部用户可见错误隐藏图片路径、URL、接口路径、base64、完整提示词和敏感信息。 +- AI帮写和生图按任务独立运行。AI帮写只使用设置的「图片理解别名」调用图片理解能力,不走②标题生成;按商品原图 `source_order` 取1至8张已下载的本地图片,在一次请求中作为同商品的多角度/细节/包装/场景证据集联合理解,超过8张时状态提示只使用前8张,原图勾选不改变输入图片。返回一份可直接编辑的商品级卖点与套图画面要求,按商品概述、可确认卖点、人群与场景、套图画面要求、待确认或避免编造的信息组织,不按图1、图2逐图说明;图片有可见差异时明确为待确认项。单图超过10MiB、总计超过32MiB、没有可用本地图、别名未配置或服务异常时不改现有卖点;图片理解读超时或网络中断提示“结果未确认,请先查看点数余额或稍后重试”,不自动重发。成功状态显示理解图片张数、扣点和余额;AI帮写期间若用户改过卖点,返回后必须确认才覆盖;全部用户可见错误隐藏图片路径、URL、接口路径、base64、完整提示词和敏感信息。 - AI帮写提交图片理解前先显示「开始AI帮写」确认框:按 `source_order` 说明会理解当前商品前1至8张可用原图并生成商品卖点与要求。模型目录只走后台读取或进程内短期缓存;仅当前图片理解别名有唯一无条件价格时显示「预计扣点:X 点」,否则明确实际以网关返回为准。确认框默认、Esc 和关闭均取消,不提交图片;开始后可取消本地等待,但已提交网关的请求仍可能产生扣点。预估不写入业务数据,完成后仍只显示接口返回的实际扣点和余额。 - 常规「生成套图」保留“已有成功历史”优先确认,选择继续后才后台读取或复用同一模型目录缓存,并显示正式生成确认。确认严格按最终 planned `specs` 展示各分类实际张数、总张数和比例;逐图主图开启时明确白底图只用第一张原图,其他分类按每张原图生成,关闭时所有分类只用第一张。仅唯一无条件的生图价格显示预计单张和总扣点,总价只按 `len(specs)` 计算;价格未知时不显示数字。默认、Esc、关闭、切换任务、取消读取或计划变化均不创建生图 job;单图失败重试和恢复未完成任务不增加这一层批量确认。 -- ⑥只管理本地图片资产,不自动上传或修改蝦皮;③线上更新边界不受影响。旧 `ImageStudioTab` 留作代码兼容但不再作为主窗口入口。 +- 商品套图只管理本地图片资产,不自动上传或修改蝦皮;③线上更新边界不受影响。旧 `ImageStudioTab` 留作代码兼容但不再作为主窗口入口。 ## 流程导航 ```text -④ 账号管理:配账号 + 启动登录(首次必做;重复点击应复用已打开 Chrome) +账号管理:配账号 + 启动登录(首次必做;重复点击应复用已打开 Chrome) │ ① 导入采集:导入 Excel → 采集旧标题/旧封面 → 自动回写旧字段(失败可手动重试) │ ② AI生成:提示词 → 选择生成标题/封面/图文(无逐条审核) │ -⑤ 设置:配置每批最大更新条数和蝦皮更新执行模式 +设置:配置每批最大更新条数和蝦皮更新执行模式 │ ③ 更新蝦皮:选择更新标题/封面/图文 → 对已生成任务点击开始更新 → 缺失内容校验 → 弹窗确认 → 账号就绪预检(未启动/未登录则中止) → 按模式换标题/封面 → 点「更新」提交 → 回写结果 → 弹窗汇总 ``` -- 未配账号 / 未登录:① 会自动确保匹配账号 Chrome 就绪但不自动登录,未登录账号略过并提示去④;③ 更新前仍只检测账号 Chrome/CDP/登录态,未启动或未登录则中止,不静默启动缺失账号 Chrome。 +- 未配账号 / 未登录:① 会自动确保匹配账号 Chrome 就绪但不自动登录,未登录账号略过并提示去账号管理;③ 更新前仍只检测账号 Chrome/CDP/登录态,未启动或未登录则中止,不静默启动缺失账号 Chrome。 - 已生成的任务即可进 ③;③ 经用户确认批量弹窗后提交线上,无常驻提交开关。 - 任意步骤失败:记入该任务、日志标明,不影响其他任务。 @@ -240,14 +240,14 @@ | `CollectTab(QWidget)` | ① | 导入、任务表、采集、回写 | | `GenerateTab(QWidget)` | ② | 左提示词管理 + 右筛选/任务列表;双击看新旧封面;开始生成/停止/进度;本轮「生成内容」下拉接入 `GenerateWorker` | | `ApplyTab(QWidget)` | ③ | 已生成任务筛选 +「更新内容」下拉 + 缺失内容阻断 +「检查本轮更新」+ 分批开始更新确认 + 检查/真实更新运行日志 + 结果回写与结束汇总 | -| `AccountsTab(QWidget)` | ④ | 账号增删改、启动登录、检测登录、生成快捷方式;首次启动复用初始卖家中心页,启动中防重复触发,登录检测把 Shopee accounts 登录页判为未登录 | -| `SettingsTab(QWidget)` | ⑤ | cmhub 网关配置 + 响应式三列设置表单 + 生成参数 + Chrome/端口配置 + 蝦皮更新执行;数据路径字段隐藏但保留配置兼容 | -| `ProductSuiteTab(QWidget)` | ⑥ | 商品套图多任务、账号+商品ID上下文、原图导入/排序、套图分类、AI帮写、cmhub 异步生成、结果历史与删除撤销 | +| `AccountsTab(QWidget)` | 账号管理 | 账号增删改、启动登录、检测登录、生成快捷方式;首次启动复用初始卖家中心页,启动中防重复触发,登录检测把 Shopee accounts 登录页判为未登录 | +| `SettingsTab(QWidget)` | 设置 | cmhub 网关配置 + 响应式三列设置表单 + 生成参数 + Chrome/端口配置 + 蝦皮更新执行;数据路径字段隐藏但保留配置兼容 | +| `ProductSuiteTab(QWidget)` | 商品套图 | 商品套图多任务、账号+商品ID上下文、原图导入/排序、套图分类、AI帮写、cmhub 异步生成、结果历史与删除撤销 | | `TaskTableModel(QAbstractTableModel)` | ①②③ | 任务表格数据模型,供 `QTableView` 使用 | | `BaseWorker(QObject)` | 后台 | 定义 `progress/log/row_updated/failed/finished/cancelled` signals | | `ApplyWorker(BaseWorker)` | ③ | 账号就绪预检、检查本轮更新、按每批最大条数分批、按账号并行或串行调用 `editor.apply_task(...)`、逐条 `set_applied()`,失败继续,写运行日志 | -| `AIModelTestWorker(BaseWorker)` | ⑤ | 后台调用 `appconfig.test_ai_model()` 测试模型连接 | +| `AIModelTestWorker(BaseWorker)` | 设置 | 后台调用 `appconfig.test_ai_model()` 测试模型连接 | | `WriteBackWorker(BaseWorker)` | ①③ | ①回写旧字段;③回写新标题/新封面/更新状态 | -| `ImageStudioPullImagesWorker / ImageStudioDownloadOriginalWorker / ProductSuiteImportImagesWorker / ProductSuiteGenerateWorker / ProductSuiteAiWriteWorker / CMHubModelCatalogWorker` | ⑥ | 后台执行只读拉主图、远程原图下载、本地图片校验复制、cmhub 套图生成、AI帮写和只读模型目录;拉图和本轮下载支持安全边界协作停止,worker 不直接操作 QWidget | +| `ImageStudioPullImagesWorker / ImageStudioDownloadOriginalWorker / ProductSuiteImportImagesWorker / ProductSuiteGenerateWorker / ProductSuiteAiWriteWorker / CMHubModelCatalogWorker` | 商品套图 | 后台执行只读拉主图、远程原图下载、本地图片校验复制、cmhub 套图生成、AI帮写和只读模型目录;拉图和本轮下载支持安全边界协作停止,worker 不直接操作 QWidget | > 采集、生成、更新都是耗时操作,使用 `QObject` worker + `QThread`。Worker 不直接操作 QWidget,只通过 signal 通知主线程刷新 UI。 diff --git a/docs/tasks/T-653.md b/docs/tasks/T-653.md index 2df6b70..f77c91d 100644 --- a/docs/tasks/T-653.md +++ b/docs/tasks/T-653.md @@ -3,7 +3,7 @@ id: T-653 title: 精简主Tab标题并移除更新蝦皮静态红点 phase: 7 deps: [] -status: TODO +status: DONE created: 2026-07-17 --- @@ -52,12 +52,12 @@ git diff --check ## 验收要点 -- [ ] 软件启动后,「③ 更新蝦皮」Tab 前不再显示静态红/警告色圆点。 -- [ ] ③没有待更新任务、失败任务或未读提醒时不会出现任何虚假的状态标记。 -- [ ] 主 Tab 标题依次为「① 导入采集、② AI生成、③ 更新蝦皮、账号管理、设置、商品套图」。 -- [ ] 进入/离开「设置」时未保存更改确认仍正确工作,账号管理和商品套图均能正常创建和切换。 -- [ ] 用户可见引导不再要求前往“④账号管理”“⑤设置”或“⑥商品套图”。 -- [ ] 不影响①采集、②AI生成、③真实更新确认、账号登录、cmhub 设置、套图生成、数据库、Excel、Shopee CDP 或自动升级。 +- [x] 软件启动后,「③ 更新蝦皮」Tab 前不再显示静态红/警告色圆点。 +- [x] ③没有待更新任务、失败任务或未读提醒时不会出现任何虚假的状态标记。 +- [x] 主 Tab 标题依次为「① 导入采集、② AI生成、③ 更新蝦皮、账号管理、设置、商品套图」。 +- [x] 进入/离开「设置」时未保存更改确认仍正确工作,账号管理和商品套图均能正常创建和切换。 +- [x] 用户可见引导不再要求前往“④账号管理”“⑤设置”或“⑥商品套图”。 +- [x] 不影响①采集、②AI生成、③真实更新确认、账号登录、cmhub 设置、套图生成、数据库、Excel、Shopee CDP 或自动升级。 ## 边界(不改什么) @@ -69,3 +69,6 @@ git diff --check ## 执行记录 - 2026-07-17:根据主Tab标题精简和更新蝦皮静态红点误导问题创建任务。待实现。 +- 2026-07-17:开始实现;先核对主窗口标题路由、静态图标、测试和用户操作文档。 +- 2026-07-17:移除 `MainWindow` 对③的静态图标设置和无调用的 `_warning_dot_icon()`;保留通配导入兼容所需的 `QIcon`、`QPixmap`、`QPainter`。`TAB_TITLES`、构建分发、设置离开守卫、账号/设置跳转及所有用户可见引导统一改为无序号的后三个主 Tab 名称;同步更新 API、架构、路由和 SVG 线框。 +- 2026-07-17:验证通过:`py -3.10 -m unittest tests.test_gui`(187 项)、`py -3.10 -m unittest discover -s tests`(578 项)、`py -3.10 -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`git diff --check`。 diff --git a/docs/ui/README.md b/docs/ui/README.md index 56d7283..eafca74 100644 --- a/docs/ui/README.md +++ b/docs/ui/README.md @@ -4,17 +4,17 @@ | 文件 | 内容 | | --- | --- | -| [overview-pipeline.svg](overview-pipeline.svg) | 主流水线总览(stage:imported→collected→generated→applied;图中旧⑥AI工场入口已由商品套图替换) | +| [overview-pipeline.svg](overview-pipeline.svg) | 主流水线总览(stage:imported→collected→generated→applied;图中旧AI工场入口已由商品套图替换) | | [tab1-import-collect.svg](tab1-import-collect.svg) | ① 导入采集:导入 Excel、任务列表、采集旧标题/旧封面、回写、日志 | | [tab2-ai-generate.svg](tab2-ai-generate.svg) | ② AI生成:提示词、生成、新旧封面对照、重置图片后批量补生成 | | [tab2-ai-generate-T584.svg](tab2-ai-generate-T584.svg) | ② AI生成(T-584 提案):标题提示词加模板控件行、封面控件压一行、插入类按钮下移、标题输入框增高 | | [tab3-update-shopee.svg](tab3-update-shopee.svg) | ③ 更新蝦皮:已生成任务、开始更新确认、执行结果、汇总 | -| [tab4-accounts.svg](tab4-accounts.svg) | ④ 账号管理:账号列表、启动登录、检测登录、账号编辑弹窗 | -| [tab5-settings.svg](tab5-settings.svg) | ⑤ 设置:AI 模型/Key、路径、端口 | -| [tab6-image-studio.svg](tab6-image-studio.svg) | ⑥ 图片精修(提案·初版):商品列表、原主图九宫、照片池、生成控制、终选排序 | -| [tab6-image-studio-v2.svg](tab6-image-studio-v2.svg) | ⑥ 图片精修(提案·重排):细图标轨、照片池放大、提示词中心整列、全宽出片盘 | -| [tab6-ai-studio-v3.svg](tab6-ai-studio-v3.svg) | ⑥ AI工场(需求定稿·v3):合并提示词、完整模板 CRUD、异步任务照片池、部分导出与终选排序 | -| [tab6-suite-package-v1.svg](tab6-suite-package-v1.svg) | ⑥ 商品套图(T-622 已实现):从「虾皮圈电商图生成器」商品套餐交互迁移并替换主界面 AI工场入口;单一套图类型,多任务标签,商品原图,平台/国家/语言/比例,AI 帮写,白底/场景/卖点及自定义分类,结果历史、重试、删除撤销与生成进度 | -| [tab6-suite-prompt-dialog-T637.svg](tab6-suite-prompt-dialog-T637.svg) | ⑥ 商品套图(T-637 提案):标题行「AI帮写/取消」紧随 label、最右新增「提示词设置」;提示词设置弹窗——左编辑基础模板(行序对齐调研笔记组装顺序,套图名称+补充描述、插入变量菜单、占位符校验、只读规则占位符内联)、右只读实时最终预览(预览分类下拉)、保存/恢复默认共用原子写盘;第一版不做同类多张序号/差异化 | +| [tab4-accounts.svg](tab4-accounts.svg) | 账号管理:账号列表、启动登录、检测登录、账号编辑弹窗 | +| [tab5-settings.svg](tab5-settings.svg) | 设置:AI 模型/Key、路径、端口 | +| [tab6-image-studio.svg](tab6-image-studio.svg) | 图片精修(提案·初版):商品列表、原主图九宫、照片池、生成控制、终选排序 | +| [tab6-image-studio-v2.svg](tab6-image-studio-v2.svg) | 图片精修(提案·重排):细图标轨、照片池放大、提示词中心整列、全宽出片盘 | +| [tab6-ai-studio-v3.svg](tab6-ai-studio-v3.svg) | AI工场(需求定稿·v3):合并提示词、完整模板 CRUD、异步任务照片池、部分导出与终选排序 | +| [tab6-suite-package-v1.svg](tab6-suite-package-v1.svg) | 商品套图(T-622 已实现):从「虾皮圈电商图生成器」商品套餐交互迁移并替换主界面 AI工场入口;单一套图类型,多任务标签,商品原图,平台/国家/语言/比例,AI 帮写,白底/场景/卖点及自定义分类,结果历史、重试、删除撤销与生成进度 | +| [tab6-suite-prompt-dialog-T637.svg](tab6-suite-prompt-dialog-T637.svg) | 商品套图(T-637 提案):标题行「AI帮写/取消」紧随 label、最右新增「提示词设置」;提示词设置弹窗——左编辑基础模板(行序对齐调研笔记组装顺序,套图名称+补充描述、插入变量菜单、占位符校验、只读规则占位符内联)、右只读实时最终预览(预览分类下拉)、保存/恢复默认共用原子写盘;第一版不做同类多张序号/差异化 | > 仅为线框效果图,最终样式以实现为准。带 `-T584`/`-v2` 等后缀的为**改版提案**,未定稿。界面职责与流程见 [../routes.md](../routes.md)。 diff --git a/docs/ui/overview-pipeline.svg b/docs/ui/overview-pipeline.svg index 68830e1..41cfcd1 100644 --- a/docs/ui/overview-pipeline.svg +++ b/docs/ui/overview-pipeline.svg @@ -15,7 +15,7 @@ - 蝦皮圈優化助手 · 5 Tab 流水线总览 + 蝦皮圈優化助手 · 核心流水线总览 工作流优先顺序 · 对象是 Excel 里的每一行任务 · 状态字段 stage 贯穿全程 @@ -64,7 +64,7 @@ - ④ 账号管理 + 账号管理 账号名/别名/端口 密码本地明文·仅参考 独立 user-data-dir @@ -74,7 +74,7 @@ - ⑤ 设置 + 设置 AI 模型/Key 本地图片目录 Chrome 路径 @@ -87,5 +87,5 @@ - ① ③ 依赖账号已配置且已登录(④);未配/未登录则执行按钮禁用并提示去④。存储:data/config.json + SQLite + data/images/。 + ①、③依赖账号管理中的已登录账号;未配置或未登录时执行按钮禁用并提示前往账号管理。存储:data/config.json + SQLite + data/images/。 diff --git a/docs/ui/tab1-import-collect.svg b/docs/ui/tab1-import-collect.svg index 3c44bf6..911a21c 100644 --- a/docs/ui/tab1-import-collect.svg +++ b/docs/ui/tab1-import-collect.svg @@ -36,8 +36,8 @@ ① 导入采集 ② AI生成 ③ 更新蝦皮 - ④ 账号管理 - ⑤ 设置 + 账号管理 + 设置 diff --git a/docs/ui/tab2-ai-generate-T584.svg b/docs/ui/tab2-ai-generate-T584.svg index 20d6fa4..2148305 100644 --- a/docs/ui/tab2-ai-generate-T584.svg +++ b/docs/ui/tab2-ai-generate-T584.svg @@ -21,8 +21,8 @@ ① 导入采集 ② AI生成 ③ 更新蝦皮 - ④ 账号管理 - ⑤ 设置 + 账号管理 + 设置 diff --git a/docs/ui/tab2-ai-generate.svg b/docs/ui/tab2-ai-generate.svg index a634e27..c8ffb90 100644 --- a/docs/ui/tab2-ai-generate.svg +++ b/docs/ui/tab2-ai-generate.svg @@ -20,8 +20,8 @@ ① 导入采集 ② AI生成 ③ 更新蝦皮 - ④ 账号管理 - ⑤ 设置 + 账号管理 + 设置 @@ -54,7 +54,7 @@ 插入标题 预览 变量 {旧标题}{新标题}{商品id}{店铺} - 模型/并发/重试/分辨率/质量 → ⑤设置 + 模型/并发/重试/分辨率/质量 → 设置 diff --git a/docs/ui/tab3-update-shopee.svg b/docs/ui/tab3-update-shopee.svg index a8a26af..e1e3d8f 100644 --- a/docs/ui/tab3-update-shopee.svg +++ b/docs/ui/tab3-update-shopee.svg @@ -16,7 +16,7 @@ ① 导入采集② AI生成 ③ 更新蝦皮 - ④ 账号管理⑤ 设置 + 账号管理设置 diff --git a/docs/ui/tab4-accounts.svg b/docs/ui/tab4-accounts.svg index 8952af6..923b143 100644 --- a/docs/ui/tab4-accounts.svg +++ b/docs/ui/tab4-accounts.svg @@ -16,8 +16,8 @@ ① 导入采集② AI生成 ③ 更新蝦皮 - ④ 账号管理 - ⑤ 设置 + 账号管理 + 设置 diff --git a/docs/ui/tab5-settings.svg b/docs/ui/tab5-settings.svg index 8d11563..36edce1 100644 --- a/docs/ui/tab5-settings.svg +++ b/docs/ui/tab5-settings.svg @@ -15,8 +15,8 @@ ① 导入采集② AI生成 - ③ 更新蝦皮④ 账号管理 - ⑤ 设置 + ③ 更新蝦皮账号管理 + 设置 diff --git a/docs/ui/tab6-ai-studio-v3.svg b/docs/ui/tab6-ai-studio-v3.svg index e87267f..bb0b5e4 100644 --- a/docs/ui/tab6-ai-studio-v3.svg +++ b/docs/ui/tab6-ai-studio-v3.svg @@ -25,8 +25,8 @@ ① 导入采集② AI生成 - ③ 更新蝦皮④ 账号管理 - ⑤ 设置⑥ AI工场 + ③ 更新蝦皮账号管理 + 设置AI工场 @@ -159,5 +159,5 @@ 打开导出目录 - v3 · ⑥ AI工场 · 不导入本地图片 · 文件名:商品ID_主图_1.jpg / 商品ID_详情图_1.jpg · v1 导出后手动上传蝦皮 + v3 · AI工场 · 不导入本地图片 · 文件名:商品ID_主图_1.jpg / 商品ID_详情图_1.jpg · v1 导出后手动上传蝦皮 diff --git a/docs/ui/tab6-image-studio-v2.svg b/docs/ui/tab6-image-studio-v2.svg index 031102a..07f1720 100644 --- a/docs/ui/tab6-image-studio-v2.svg +++ b/docs/ui/tab6-image-studio-v2.svg @@ -28,9 +28,9 @@ ① 导入采集 ② AI生成 ③ 更新蝦皮 - ④ 账号管理 - ⑤ 设置 - ⑥ 图片精修 + 账号管理 + 设置 + 图片精修 diff --git a/docs/ui/tab6-image-studio.svg b/docs/ui/tab6-image-studio.svg index 54a74a0..2594da7 100644 --- a/docs/ui/tab6-image-studio.svg +++ b/docs/ui/tab6-image-studio.svg @@ -24,15 +24,15 @@ 蝦皮圈優化助手 · 蝦皮商品批量改标题换封面 - + ① 导入采集 ② AI生成 ③ 更新蝦皮 - ④ 账号管理 - ⑤ 设置 - ⑥ 图片工场 + 账号管理 + 设置 + 图片工场 @@ -239,7 +239,7 @@ 封面① - ② ③ ④ ⑤ …空槽虚线 + 其余位置为空槽虚线 详情图 3/12 diff --git a/docs/ui/tab6-suite-package-v1.svg b/docs/ui/tab6-suite-package-v1.svg index c27ae56..91f4a15 100644 --- a/docs/ui/tab6-suite-package-v1.svg +++ b/docs/ui/tab6-suite-package-v1.svg @@ -1,5 +1,5 @@ - +