Files
cmshoppe/docs/04-architecture.md
T

60 KiB
Raw Blame History

架构设计

本文讲“怎么把技术栈搭起来”:模块职责、存储模型、流水线、CDP 已验证事实、开发顺序。 具体用了哪些库 / 平台,见 技术栈。

一、系统结构

Windows 本地桌面自动化工具,无后端服务。当前正式 GUI 显示 5 个工作流 Tab;AI工场图片工作区的代码、数据库与本地资产能力保留,但主界面入口暂时隐藏。

运营(人)
  |
  v
GUI(PySide6 QTabWidget,当前显示 5 Tab)
  ① 导入采集  ② AI生成  ③ 更新蝦皮  ④ 账号管理  ⑤ 设置
  |
  v
核心模块(Python)
  ├── appconfig  读应用配置 data/config.json(Chrome 路径、目录根、AI 配置、端口范围…)
  ├── db         SQLite 读写:账号、任务、各阶段结果(cmshopee.db)
  ├── excel      openpyxl 导入输入列 / 回写输出列到原 Excel
  ├── config     账号 ↔ user-data-dir 绑定、slug、目录创建
  ├── accounts   账号 CRUD 服务:生成 slug/目录、启动登录、检测登录
  ├── chrome     按账号拼启动参数、启动/探测 Chrome、生成快捷方式
  ├── cdp        CDP 客户端(连接、找/开 tab、执行 JS、拖拽)
  ├── editor     登录检测 / 采集旧标题旧封面 / 改标题 / 换封面 / 点更新
  ├── ai         文本生成(提示词+旧标题→新标题)/ 图像生成(提示词+旧封面→新封面)
  ├── image_studio          AI工场项目/资产/job/终选顺序数据服务
  ├── image_studio_images   远程原图安全下载、缩略图、原图落盘
  ├── image_studio_generation cmhub 托管多图异步 submit/poll/download 编排
  └── image_studio_export   终选图片本地 JPEG 转码、目录安全导出
  |
  v
Google Chrome(每账号独立 --user-data-dir + --remote-debugging-port) + AI 服务(默认 cmhub 网关;direct 仅内部兼容/回滚)
  |
  v
Shopee 卖家中心页面 / 本地图片目录

真实组件:

  • GUI 入口:根目录 main.py 调用 app/gui/ 包(PySide6 + QMainWindow + QTabWidget,当前正式界面显示 5 Tab);包入口 app/gui/__init__.py 提供 main() 并兼容 from app import gui / from app.gui import MainWindow;也支持 python -m app。ImageStudioTab 及其服务层仍保留在代码和发布包中,但当前不加入顶层 QTabWidget。
  • 核心模块统一放在正式代码包 app/:appconfig.py、db.py、excel.py、config.py、accounts.py、chrome.py、editor.py、workers.py、ai.py、prompts.py、image_studio.py、image_studio_images.py、image_studio_generation.py、image_studio_export.py;CDP 底座迁入 app/cdp.py(当前根目录 cdp.py 为已验证来源)。
  • 已验证脚本(重构进模块):prototypes/demo.py、prototypes/set_title.py、prototypes/set_cover.py、prototypes/get_title.py、prototypes/cookies.py、prototypes/inspect_images.py、prototypes/grab.py。
  • 外部依赖:本机 Google Chrome;Shopee;AI 服务(文本+图像;普通产品默认 cmhub 网关,由 data/config.json 的 ai.cmhub + data/config/cmhub.json 配置;direct 直连模型清单仅作为内部兼容/手工回滚路径保留);openpyxl。

二、流水线(核心)

每个任务(Excel 行)依次走过 4 个业务阶段,状态字段 stage 贯穿全程:

imported  →  collected  →  generated  →  applied
(导入 Excel)  (采集旧数据)   (AI 生成新数据)  (改 Shopee 并提交)
   ① Tab        ① Tab          ② Tab          ③ Tab
  • imported:openpyxl 解析输入列入库。
  • collected:只读打开商品页,读旧标题、下载旧封面到本地,写 old_title/old_cover_path,回写 Excel 旧字段。
  • generated:AI 按②「生成内容」下拉生成新标题和/或新封面;选择只生成标题时 new_cover_path=NULL;选择只生成封面时不调用生文,封面标题上下文取 new_title or old_title,只写 new_cover_path,因此“有新封面、无新标题”是合法组件状态;选择生成标题和封面时可按缺失组件增量补齐。不设逐条人工审核阶段,生成成功即可进入 ③;③ 再按「更新内容」下拉决定只更新标题、只更新封面或更新图文。
  • applied:③ 点击「开始更新」后弹窗确认当前筛选范围和任务数量;确认后打开编辑页换标题+封面,逐条点「更新」提交线上,回写结果。

任意阶段失败 → stage 不前进、记 error、result/skipped,不影响其他任务。

三、职责划分

GUI(当前显示 5 Tab):见 routes.md。只做交互与预览,不写业务逻辑;耗时操作走 PySide6 QObject worker + QThread,用 signal 回主线程刷新 UI。① 采集点击后会为本轮匹配到的账号自动确保 Chrome 就绪:已打开则复用,未打开才启动;随后只检测登录态,未登录账号的任务跳过并汇总提示去 ④人工登录。③ 更新蝦皮仍是线上提交高风险链路:执行前只检测账号 Chrome/CDP/登录态,不自动启动缺失账号 Chrome。⑥ AI工场入口当前隐藏;其只读拉图、下载图片和 cmhub 托管生成代码仍保留,且不会自动上传蝦皮。

核心模块

  • appconfig:读写 data/config.json(Chrome 路径、chrome_user_data_dir 根、图片目录、AI 配置、端口、DB 路径)并把默认相对路径解析到 data/。
  • db:SQLite 读写账号、任务、各阶段结果;建表/迁移。
  • excel:openpyxl 读输入列、把输出列回写原 Excel(处理文件锁)。
  • config:账号 ↔ user-data-dir 绑定;slug;目录创建。
  • accounts:账号 CRUD 服务;生成目录、启动登录、检测登录;不自动登录/填密码。
  • chrome:拼接启动命令、启动、探测端口、(可选)生成快捷方式。
  • cdp:连接调试端口、找/开 tab、执行 JS、拖拽、注入文件。
  • editor:登录检测、采集(读旧标题、下载旧封面)、改标题、换封面、点更新。
  • ai:gen_title(prompt, old_title)、gen_cover(prompt, old_cover_path)(外部 AI;默认走 cmhub 网关,保留 direct 模型清单作为内部兼容/回滚 backend;返回值保持标题字符串/本地 JPEG 路径)。

存储(同一事实只存一处)

  • 应用配置(模型选择、生成参数、目录、Chrome 路径)→ data/config.json。
  • AI 模型清单(direct 内部兼容模式 url/模型/密钥/类型/连接超时)→ data/config/ai_models.json(API Key 本地明文保存,必须 gitignore,UI 打码显示;普通设置页不再暴露 direct 切换入口)。
  • cmhub 网关 Key → data/config/cmhub.json,schema { "api_key": "..." };config.json 只保存 Base URL、别名和超时,不保存 Key。
  • 业务数据(账号、任务、各阶段结果)→ 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/chrome_user_data_dir/<slug>/。

T-538 后统一数据根为 data/:打包版默认 <exe目录>/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/ 可写。

四、多账号隔离方案(决策)

采用每账号独立 user-data-dir(非 Chrome profile)。--remote-debugging-port 绑定在 user-data-dir/进程上,profile 方案无法每账号独立 CDP、串号风险高。启动主路径用程序 subprocess 直启(--remote-debugging-port + --remote-allow-origins=* + --user-data-dir);可选生成 .lnk 快捷方式(PowerShell WScript.Shell,参数写在「目标」字段)。

同一账号重复点击④「启动登录」不得重复执行 subprocess.Popen。正确流程是先探测该账号 debug_port 的 /json/version:端口未响应才按上述启动参数新开 Chrome;端口已响应则复用该账号现有 Chrome/CDP,打开或激活卖家中心登录 tab 供人工登录,并记录为复用,不创建第二个账号窗口。

五、数据模型

5.1 应用配置 data/config.json

{
  "chrome_path": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
  "user_data_root": "chrome_user_data_dir",
  "image_dir": "images",
  "db_path": "cmshopee.db",
  "default_debug_port": 9222,
  "debug_port_range": [9222, 9260],
  "cdp_ready_timeout": 60,
  "ai": {
    "default_text_model": "GPT-5.5 文本",
    "default_image_model": "Nano Banana 2",
    "generate_cover": false,
    "generate_mode": "title",
    "backend": "cmhub",
    "cmhub": {
      "base_url": "",
      "title_alias": "",
      "image_alias": "",
      "connect_timeout": 66,
      "check_balance_before_batch": false
    },
    "title_concurrency": 4,
    "image_concurrency": 4,
    "retry": 2,
    "jpg_quality": 90,
    "resolution": "1k",
    "resolution_timeouts": { "512": 180, "1k": 240, "2k": 360, "4k": 600 }
  },
  "shopee_update": {
    "test_item_id": "51100639510",
    "update_mode": "title",
    "max_items_per_run": 1,
    "dry_run": false,
    "max_parallel_accounts": 1
  }
}

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 为 GET /api/v1/models 发现的能力别名,connect_timeout 为连接超时;API Key 不在此处保存。⑤设置页与⑥AI工场只展示 cmhub 托管“默认档 / 高质量档 / 省点档”、展示名、用途和扣点提示,不展示 OpenAI Key、Provider URL、上游接口路径或直连模型 slug;执行层仍只保存 alias。
  • 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。
  • 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、不入日志/导出。

shopee_update 段放③ 更新蝦皮执行参数:

  • test_item_id:历史/调试兼容字段;默认 51100639510。普通正式更新不再以该字段限制商品 ID,也不因当前筛选结果包含非测试商品而阻断;后续如需要调试模式,可单独启用测试商品限制。
  • update_mode:③「更新内容」下拉的主字段,取值 title / cover / title_cover,分别表示只更新标题、只更新封面、更新标题和封面;默认 title。
  • max_items_per_run:每批最大更新任务数;默认 1,当前筛选结果超过该值时自动分批,不再按总数阻断。
  • dry_run:内部兼容字段;普通用户界面不展示该开关,③「检查本轮更新」按钮触发检查模式,只写运行日志,不打开 Shopee、不点击「更新」、不改任务状态;默认 false。
  • max_parallel_accounts:最多同时执行的账号数,范围 1..5;1 表示逐个账号串行,>=2 表示按账号分组并行,同一账号内仍按任务串行;默认 1。旧配置中的并行布尔开关为关闭时会迁移为 max_parallel_accounts=1,开启时会保留旧数量并夹紧到 1..5,保存后不再写回旧并行布尔字段。

该段不是替代 ③ 确认弹窗的常驻授权;③「开始更新」仍必须弹窗确认,用户点是后才执行。dry_run=true 时不会真实提交;dry_run=false 时以③确认弹窗作为线上提交前的唯一显式确认边界。普通正式更新不读取 test_item_id 做阻断。旧配置中的真实提交、封面更新、成功关页等开关只作迁移兼容读取,保存后不再写回。

5.1b AI 模型清单 data/config/ai_models.json

模型定义清单("有哪些模型"),与 config.json 的 ai 段("选了哪个 + 全局参数")职责分开。该文件只用于内部兼容 backend=direct;普通用户默认 backend=cmhub,生文/生图使用 cmhub 别名,不读取此文件。

{
  "models": [
    {
      "name": "Nano Banana 2",        // 服务商/模型名,唯一,作下拉显示与引用键
      "category": "image",            // text | image —— 决定它出现在“标题/图片大模型”哪个下拉
      "enabled": true,
      "url": "https://api.vectorengine.ai/v1/chat/completions",
      "model": "gemini-3.1-flash-image-preview",   // 模型 ID
      "api_key": "***",               // 密钥:本地明文存、保存/变更时提示、UI 打码、不入日志/导出、不进版本库
      "api_type": "auto",             // chat | images_edits | auto —— 决定请求构造方式
      "connect_timeout_seconds": 30,  // 连接该服务超时(每模型,默认 30)
      "timeout_seconds": 0,           // 返回超时:0/留空 = 运行时按 resolution_timeouts 取值
      "extra_body": {}
    }
  ]
}

关键事实:

  • category(文本/图像)是必需的:角色下拉据此过滤(标题下拉只列 text、图片下拉只列 image),防止错配。
  • 约束:至少各有一个 text 与一个 image 模型;下拉默认最少一项、删到剩一项时禁用「删除」。
  • connect_timeout_seconds(连接超时)属于模型;返回超时由分辨率映射决定(不在模型上单设)。
  • api_type 反映不同 API 形状(chat/images_edits/auto),请求构造按它分支。
  • name 唯一;api_key 本地明文保存、保存/变更时弹窗提示、打码显示。
  • 日志/状态/导出不得含密码或 API Key;结构化对象统一先过 appconfig.sanitize_for_log(),自由文本只允许在掌握明文值时用 appconfig.redact_secrets() 替换。

5.1c cmhub Key data/config/cmhub.json

{ "api_key": "sk_cmhub_xxx" }
  • 文件必须 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。
  • cmhub /models 如返回 display_name/tags/recommended_for/tier/prices,GUI 优先用这些字段生成中文档位说明;缺少这些字段时按 alias/tag 的保守规则兜底到“默认档”。客户端不得把 OpenAI 原始模型名作为默认执行事实。

5.2 SQLite cmshopee.db

-- 批次(一次导入动作)
CREATE TABLE batches (
  id                TEXT PRIMARY KEY,       -- batch_id,如 20260626_153000_xxxx
  source_files_json TEXT NOT NULL,          -- 导入文件绝对路径列表(JSON)
  status            TEXT NOT NULL DEFAULT 'active', -- active/done/partial/failed
  note              TEXT,
  created_at        TEXT NOT NULL,
  updated_at        TEXT NOT NULL,
  deleted_at        TEXT,                  -- T-206 软删除时间;默认业务查询排除
  deleted_reason    TEXT                   -- 软删除原因/来源
);

-- 账号(④ 账号管理)
CREATE TABLE accounts (
  id            INTEGER PRIMARY KEY,
  account_name  TEXT NOT NULL,          -- Shopee 登录账号名(展示/参考)
  alias         TEXT UNIQUE NOT NULL,   -- 别名,Excel 用它匹配
  region_host   TEXT NOT NULL,
  slug          TEXT UNIQUE NOT NULL,   -- user-data-dir 子目录名 [a-z0-9_]
  user_data_dir TEXT NOT NULL,
  debug_port    INTEGER NOT NULL,
  password      TEXT,                   -- 本地明文,仅参考,不自动登录;保存/变更时提示;UI 打码,gitignore
  note          TEXT,
  created_at    TEXT NOT NULL,
  updated_at    TEXT NOT NULL,
  last_login_at TEXT
);

-- 任务 + 各阶段结果(贯穿流水线)
CREATE TABLE tasks (
  id              INTEGER PRIMARY KEY,
  batch_id        TEXT NOT NULL REFERENCES batches(id),
  -- Excel 回写定位
  source_file     TEXT NOT NULL,          -- 展示用原路径
  source_file_abs TEXT NOT NULL,          -- 绝对路径,作为回写分组依据
  source_sheet    TEXT NOT NULL,          -- sheet 名
  source_row      INTEGER NOT NULL,       -- Excel 行号(1-based)
  row_key         TEXT NOT NULL UNIQUE,   -- batch/file/sheet/row 组成,防重复导入
  -- 输入列(Excel)
  account_name    TEXT,
  alias           TEXT NOT NULL,
  item_id         TEXT NOT NULL,
  -- 采集输出(程序写,改前快照)
  old_title       TEXT,
  old_cover_path  TEXT,                   -- 旧封面本地图片路径
  -- AI 输出
  new_title       TEXT,
  new_cover_path  TEXT,                   -- 新封面本地图片路径
  image_task_id   TEXT,                   -- cmhub 异步生图任务ID;用于重启/重试续查
  image_task_key  TEXT,                   -- 本次生图提交幂等键;用于 submit 网络抖动重发
  -- 应用
  committed       INTEGER NOT NULL DEFAULT 0, -- 是否成功点「更新」提交
  stage           TEXT NOT NULL DEFAULT 'imported', -- imported/collected/generated/applied
  status          TEXT NOT NULL DEFAULT 'pending',  -- pending/running/success/failed/skipped/cancelled
  last_error      TEXT,
  collect_attempts  INTEGER NOT NULL DEFAULT 0,
  generate_attempts INTEGER NOT NULL DEFAULT 0,
  apply_attempts    INTEGER NOT NULL DEFAULT 0,
  imported_at     TEXT NOT NULL,
  collected_at    TEXT,
  generated_at    TEXT,
  applied_at      TEXT,
  updated_at      TEXT NOT NULL,
  UNIQUE(batch_id, source_file_abs, source_sheet, source_row)
);

CREATE INDEX idx_tasks_batch_stage_status ON tasks(batch_id, stage, status);
CREATE INDEX idx_tasks_alias ON tasks(alias);
CREATE INDEX idx_tasks_item ON tasks(item_id);

运行日志(T-504):

CREATE TABLE run_logs (
  id             INTEGER PRIMARY KEY,
  run_type       TEXT NOT NULL,        -- apply 等
  dry_run        INTEGER NOT NULL DEFAULT 0,
  status         TEXT NOT NULL DEFAULT 'running', -- running/done/blocked/cancelled
  total          INTEGER NOT NULL DEFAULT 0,
  done           INTEGER NOT NULL DEFAULT 0,
  success_count  INTEGER NOT NULL DEFAULT 0,
  skipped_count  INTEGER NOT NULL DEFAULT 0,
  failed_count   INTEGER NOT NULL DEFAULT 0,
  options_json   TEXT,
  summary_json   TEXT,
  started_at     TEXT NOT NULL,
  finished_at    TEXT
);

CREATE TABLE run_log_events (
  id         INTEGER PRIMARY KEY,
  run_id     INTEGER NOT NULL REFERENCES run_logs(id) ON DELETE CASCADE,
  task_id    INTEGER,
  alias      TEXT,
  item_id    TEXT,
  level      TEXT NOT NULL DEFAULT 'info',
  message    TEXT NOT NULL,
  created_at TEXT NOT NULL
);

5.2c 诊断日志分层(T-207 + T-505 已接入全流程)

当前问题:① 采集旧标题/旧封面出现单条失败时,现有代码只把该任务写成 status=failed、last_error=<错误>,并在状态栏显示失败计数;但没有记录“卡在哪一步”,也没有完整 traceback,后续 debug 难以判断是打开商品页、页面就绪、读标题、读封面 URL、下载图片还是写库/回写失败。

采用两层日志:

  • SQLite 运行日志(业务可读):复用 run_logs/run_log_events,用于 GUI 查看和运营排查。run_type 扩展到 collect/generate/import/write_back/apply/chrome_launch/login_check/ai_model_test。事件必须带 task_id/alias/item_id(能拿到时),message 统一包含 step=<步骤>、结果和简短错误;不新增敏感字段,不写 Cookie、密码、API Key、token。
  • 本地诊断 log 文件(开发调试):写入 data/logs/cmshopee.log 或按日期滚动文件,保存脱敏后的 traceback、异常类型、步骤、耗时和必要上下文。data/logs/ 必须 gitignore;自由文本异常进入日志前要用 appconfig.redact_secrets(),结构化 payload 先过 sanitize_for_log()。

优先级:

  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,业务日志和状态栏只写脱敏短错误。
  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 值。

原则:数据库日志给运营和 GUI 看“哪个商品、哪个步骤、为什么失败”;本地 log 文件给开发看完整错误栈。两者都必须脱敏。 关键事实:

  • alias 是账号↔任务唯一关联键;找不到账号 → stage=skipped,error=别名未匹配,最后弹窗汇总。
  • account_name 仅展示/参考;匹配以 alias 为准。
  • source_file_abs/source_sheet/source_row 是回写 Excel 的权威定位;即使商品 ID 重复,也按原行回写。
  • row_key 防止同一批次内重复导入同一行。
  • old_title/old_cover_path:程序采集阶段抓取的快照(输出)。
  • new_title/new_cover_path:AI 生成的两个独立组件结果(输出)。③只更新标题要求 new_title,只更新封面要求 new_cover_path,更新图文才同时要求二者;只生成封面允许 new_title=NULL,已采集的 old_title 只作为封面prompt语义参考,不回填 new_title。不设逐条确认阶段。
  • stage 表示已完成到哪个业务阶段;status 表示当前处理结果。失败时 stage 保持在最后成功阶段,status=failed,错误写 last_error。
  • committed:③ 用户确认批量更新弹窗后逐条点「更新」提交;该字段记录提交是否成功。
  • 各字段每阶段处理完立即写回 SQLite(实时落库);阶段结束后批量回写 Excel。

5.2b SQLite 并发与连接规则

  • db.connect() 统一设置:PRAGMA foreign_keys=ON、PRAGMA journal_mode=WAL、PRAGMA busy_timeout=5000、PRAGMA synchronous=NORMAL。
  • SQLite connection 不跨线程共享;每个 PySide6 worker / 后台线程按需创建自己的 connection。
  • 写操作使用短事务:单条任务完成即提交,避免长时间占用写锁。
  • 写库遇到 locked/busy:按短间隔重试,超过 busy timeout 后返回明确错误并通过 signal 通知 GUI。
  • GUI 不直接缓存“事实状态”;写库成功后由 worker 发 row_updated,GUI 再刷新对应行。
  • Excel 回写以 SQLite 为事实来源;原 Excel 被锁时不回滚 SQLite,只提示关闭后重试或另存副本。

5.3 Excel 模板

标准空模板文件保留在仓库根目录:shopee待处理任务模板.xlsx,单工作表名为 待处理任务。运营使用时应复制一份再填写,填写后的业务 Excel 含商品数据,不提交版本库。

列 含义 输入/输出
账号名 Shopee 账号名(展示) 输入
别名 关联 accounts.alias(匹配键) 输入
商品id item_id 输入
旧标题 采集到的旧标题 输出,回写
旧封面图片路径 采集到的旧封面本地路径 输出,回写
新标题 AI 生成的新标题 输出,回写
新封面图片路径 AI 生成的新封面本地路径 输出,回写
更新状态 处理结果(成功/失败/跳过+原因) 输出,回写
  • 输入列(账号名/别名/商品id)运营填;其余为程序各阶段输出,回写到原 Excel(被锁→提示重试/另存副本)。
  • 别名以此列为匹配权威,不解析文件名。
  • 标准模板推荐前三列表头固定为:账号名 | 别名 | 商品id。不要使用 账号、店铺名、店名 代替匹配列;别名 必须与 ④ 账号管理中的 accounts.alias 一致。
  • 导入容错:缺少必需列(别名/商品id)时拒绝整个文件,记入 file_errors,不导入该文件任何行。
  • 脏行处理:单行缺别名/商品id、商品id 格式错误等逐行跳过,计入 invalid,不阻塞同文件其他有效行。

5.4 本地图片目录

data/images/<batch_id>/<slug>/<task_id>_<item_id>_old.<ext>    # 采集下载的旧封面
data/images/<batch_id>/<slug>/<task_id>_<item_id>_new.<ext>    # AI 生成的新封面

路径记入 DB 后以 old_cover_path/new_cover_path 为权威;上传新封面使用 DB 中的本地绝对路径(Windows 绝对路径传 setFileInputFiles)。历史数据里的 images/<slug>/<item_id>_*.jpg 路径继续有效,不做强制迁移;T-538 后新采集/新生成默认写入 data/images/ 下的按批次细分目录。

六、关键流程细节

6.0 GUI 线程模型(PySide6)

  • 主线程只运行 QApplication、窗口、表格、弹窗和状态刷新;不得在主线程执行 CDP、AI、Excel 回写、图片下载等耗时任务。
  • 每类耗时流程封装为 QObject worker:CollectWorker、GenerateWorker、ApplyWorker、ImportWorker、WriteBackWorker。
  • Worker 通过 signal 向 GUI 汇报:progress、row_updated、log、failed、finished、cancelled;GUI 槽函数只做 UI 刷新和按钮状态切换。
  • Worker 不直接操作任何 Qt widget,不弹窗;需要用户确认的动作(如 ③ 开始更新确认)必须在主线程先完成,再启动 worker。
  • 停止 使用协作式取消:GUI 调用 worker 的 cancel 标记;worker 在任务间/重试前检查,取消未开始项,正在执行的单条任务跑到安全边界后结束。
  • SQLite connection 不跨线程共享;每个 worker/线程按需创建自己的连接,写库后发 signal 通知 UI 刷新。

6.1 采集(① Tab,只读)

  • 用账号 Chrome 打开商品页,等就绪,读旧标题(标题输入框 value)。

  • open_product 先复用已打开的同商品 tab;没有才新建商品编辑页 tab。采集完成后只关闭本次程序自动新建的商品 tab,不关闭用户原本已经打开的 tab。CDP.close() 只断开 WebSocket 控制连接,不等于关闭浏览器 tab。

  • ①采集调用 open_product(..., bring_to_front=False),不主动执行 Page.bringToFront;新建商品 tab 时尝试 Target.createTarget(background=true) 降低 Chrome 抢焦点概率,若当前 Chrome/CDP 不接受该参数则退回普通新建 tab。③更新是上传、拖拽和线上提交流程,每条任务均以前台方式打开或激活当前商品 tab,优先保障页面交互稳定;后台态失败安全恢复逻辑仅为兼容直接调用保留,正常③批量路径不依赖它。

  • 采集前和采集中途的登录检测必须区分“明确未登录”和“暂时不确定”。明确 LOGIN_PAGE / 登录页 URL 才整组略过该账号后续任务;NO_SESSION_COOKIE、检测超时或 CDP 短暂异常只记录为不确定并继续尝试采集当前商品,不得级联跳过同账号剩余任务。

  • 若商品 ID 失效、无权限或店铺不匹配导致商品编辑页无法就绪,open_product 必须读取/捕获 Shopee toast,把最近错误文案写入采集失败原因和诊断日志,不能只返回泛化超时。①列表只在明确捕获商品失效类 toast 时把“阶段”显示为“商品失效”;底层 stage 不新增中文值。若这个失败发生在程序自动新建的商品 tab 内,open_product 要关闭该 tab;复用用户已有 tab 不关闭。

  • 旧封面:取第一张 itembox 的 img.src(CDN 链接),下载到 data/images/<batch_id>/<slug>/<task_id>_<item_id>_old.jpg。

  • 写 old_title/old_cover_path、stage=collected;批量回写 Excel 旧字段。

  • 采集任务结束且本轮有成功采集行时,自动触发当前批次旧字段回写;原 Excel 被锁时不影响 SQLite 结果,提示关闭后重试,并保留手动「回写旧数据到 Excel」入口。

6.2 AI 生成(② Tab)

单个「开始生成」按钮,按本轮「生成内容」模式生成标题、封面或图文:

  1. 并发生成标题:线程池大小 = title_concurrency,调 gen_title(标题提示词, old_title) → new_title。默认 backend=cmhub,调用 POST /api/v1/generate/title 并使用 title_alias;backend=direct 仅内部兼容时使用 default_text_model。
  2. 若②「生成内容」包含封面:接着并发生成封面,线程池大小 = image_concurrency,调 generate_batch() 的封面阶段 → 新图存 data/images/<batch_id>/<slug>/<task_id>_<item_id>_new.jpg。选择 cover 时处理缺新封面且存在 new_title or old_title 的任务,优先用新标题、否则只在prompt上下文中用旧标题回退;选择 title_cover 时仍先补新标题,再按缺失组件补封面,不使用旧标题绕过失败的生文。默认 backend=cmhub 的②批量生成走异步任务接口:先 POST /api/v1/generate/image/tasks 提交并预扣点,带 Idempotency-Key 与 X-Client-Version,submit 成功后立即把 task_id 写入 tasks.image_task_id;再循环 GET /api/v1/generate/image/tasks/{task_id} 轮询,succeeded 后取 result.image_url 并安全下载转本地 JPEG。若本地已有 image_task_id,下次开始生成直接续查,不重新 submit、不重复扣点。gen_cover() 单独直接调用无 DB 上下文,仍保留旧同步 POST /api/v1/generate/image 兼容路径;backend=direct 仅内部兼容时使用 default_image_model。用户日志只展示“cmhub 托管档位 / 别名 / 扣点 / 余额 / call_id”,不得输出 cmhub 内部接口路径、Provider 请求体、Authorization、Key 或完整 prompt。
    • T-545/T-564 已实现:cmhub 模式下不再直接按用户填写的 image_concurrency 全量打到网关;实际 submit+poll 在途并发 = min(image_concurrency, 5)。拿到 image_url 后交给独立下载/保存线程池,下载线程数量与实际生图并发一致,同样最大 5。这样批量时“下一批生图任务”和“上一批图片下载/保存”可以流水线并行,但不会对 cmhub 生图任务接口或 /media/generated/images/*.png 静态下载打出超过 5 的并发。direct 兼容路径暂不改变。
    • cmhub 生图连接超时取 ai.cmhub.connect_timeout(默认 66 秒);异步 submit 读取等待 30 秒,poll 单次读取等待 15 秒,本地总等待预算 900 秒,撞预算或用户停止时保留 image_task_id/image_task_key 供下次续查;poll 返回 failed/expired(cmhub 已退点)时清空 image_task_id/image_task_key,后续重试会生成新的幂等键并重新 submit。下载层读取等待 900 秒,最多安全重试 3 次,只复用同一个 image_url,不重新请求 cmhub 生图;下载总耗时超过 20 秒时写“图片下载较慢”警告。backend=direct 兼容路径仍按模型 timeout_seconds 或 resolution_timeouts[resolution](512→180/1k→240/2k→360/4k→600)取返回超时。
    • T-548 已实现:cmhub 图片下载后端新增 ai.cmhub.download_with_curl(auto/true/false,默认 auto)。Windows 且检测到系统 curl 时优先用 curl 下载,否则或 curl 执行失败时回退 requests;生成、models、balance 仍走共享 requests Session。curl 下载前仍执行公网 URL 校验;URL 写入临时 curl 配置文件并通过 -K 传入,避免带 token 的 image_url 出现在进程命令行;use_system_proxy=false 时 curl 加 --noproxy "*"。
    • cmhub 返回的完整 image_url 默认只在内存中临时用于下载,不写入 tasks 或 run_log_events。本机调试时可设置环境变量 CMSHOPEE_DEBUG_CMHUB_IMAGE_URL=1,②本轮可见运行日志会显示脱敏后的 URL 调试行,且该行不持久化到 SQLite。
  3. 若②「生成内容」为只生成标题:标题成功后立即写 new_title,new_cover_path=NULL,不渲染封面提示词、不调用 gen_cover()、不创建本地新封面文件。
  • 失败重试:每次调用失败按 retry 次重试,仍失败则记 error(不阻塞其余)。
  • 只生成标题模式在标题成功后立即 set_generated(task_id, new_title, existing_cover_path) 写库;只生成封面模式不调用生文,封面成功通过 set_generated_cover(task_id, new_cover_path) 只写封面并保留 new_title(包括保持NULL);生成标题和封面模式先保存标题,封面成功再用组件级helper写封面。三种模式都实时落库,停止或崩溃不丢已生成结果。
  • 「停止」:取消未开始的任务,正在跑的少量完成或中断;停止后可再次「开始生成」对剩余继续。
  • 进度:标题和图片两条进度分开显示;只生成标题时图片进度显示本轮未生成/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”。
  • ⑥AI工场固定使用⑤保存的 cmhub 生图 alias。生成区显示“cmhub 托管默认档 / 高质量档 / 省点档”、当前生图别名、余额和扣点;档位是产品说明层,真实模型可由 cmhub 后台调整,客户端不保存 OpenAI slug。

提示词管理:

  • 标题提示词:data/title_prompt.txt 是当前工作文本,「保存标题提示词」写入该文件;软件启动时加载该文件回显到输入框(缺失则空)。标题命名模板另存于 data/prompts/title/*.txt,左侧模板行提供下拉、新建、保存模板、另存为、重命名、删除;模板只负责被用户选中后载入编辑框,或把当前编辑框内容保存为命名模板,不改变生成读取路径,也不在启动时覆盖 title_prompt.txt。T-549 后标题提示词支持 {旧标题} 占位符:若提示词含 {旧标题},生成前替换为该任务旧标题且不再自动追加旧标题块;若不含,则保持旧行为自动追加“旧标题:...”块。两种情况都会保留“请只返回新标题,不要解释。”输出约束。
  • 封面提示词:多模板。左侧模板行提供下拉(读 data/prompts/cover/*.txt)、新建、保存模板、模板操作(另存为/重命名/删除);重名校验、删除二次确认、删空给默认。
  • 变量:标题提示词本阶段只支持 {旧标题},左侧按钮「插入旧标题」在标题提示词光标处插入 {旧标题}。封面提示词支持占位符 {旧标题}、{新标题}、{商品id}、{店铺},生成前用该任务真实值替换(render_prompt)。「插入标题」= 在封面提示词光标处插入 {新标题};「预览」= 用某条任务的值替换封面变量后展示,确认实际发送给 AI 的内容。

6.3 应用更新(③ Tab)

  • ③ 顶部筛选确定本次作用范围;点击「开始更新」后弹窗展示筛选条件、任务数量和“将提交线上”的风险提示。

  • ③ 左下角提供「更新内容」下拉:只更新标题 / 只更新封面 / 更新标题和封面。点击「开始更新」或「检查本轮更新」时,先按当前模式检查当前筛选任务是否已经具备 new_title / new_cover_path;缺少所选内容时直接中文弹窗阻断,不创建 ApplyWorker、不打开 Chrome、不写失败状态。

  • ③ 提供「检查本轮更新」按钮:只读取当前筛选结果和写运行日志,不打开 Shopee、不提交、不改任务状态;检查汇总展示总数、店铺分布、每批最大条数、预计批次数、更新内容和略过原因。

  • 弹确认前先读取 data/config.json 的 shopee_update 执行参数。普通正式更新不再检查 test_item_id 或旧真实提交开关,当前筛选结果可以包含多个真实商品 ID。max_items_per_run 作为每批最大任务数,当前筛选总数超过该值时自动分批,不再按总数阻断。

  • 用户在③确认弹窗点「是/确认」才开始批量更新;点「否/取消」不执行、不改库。

  • 真实更新前必须做账号就绪预检:按当前筛选结果汇总需要的账号;无账号、账号 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,端口冲突直接阻断。

  • 「检查本轮更新」只写 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,必须提示线上已提交过、本地重置不回滚蝦皮、重复更新会再次提交,并保留 committed 历史事实/运行日志。

  • 若商品页是本轮程序自动新建,apply_task() 结束时成功/失败都关闭该商品编辑页;成功提交后关闭前等待 2 秒,便于 Shopee 成功状态渲染。复用用户已有 tab 时只断开 CDP,不关闭页面。open_product 内部打开失败的新建 tab 仍由 open_product 自行关闭。

6.4 登录检测

  • 无 Shopee tab 时打开卖家中心根地址 https://<region_host>/(默认 https://seller.shopee.tw/),重定向到登录页或缺会话 Cookie(SPC_ST/SPC_U)→ 未登录;不自动登录,提示人工登录。
  • ④「启动登录」只负责准备该账号独立 user-data-dir + CDP 端口的 Chrome,供用户人工登录;该入口必须幂等:若端口已响应,复用现有账号 Chrome 并打开/激活卖家中心登录 tab,不再新开 Chrome;若端口未响应,才启动 Chrome。「检测登录」只验证当前 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 已验证事实(务必遵守)

难点 已验证结论
Chrome 启动参数 全关后带 --remote-debugging-port=<port> --remote-allow-origins=* --user-data-dir=<dir>;缺 allow-origins 则 WebSocket 403
代理干扰 清除 *_proxy(requests trust_env=False),否则连本地 CDP 超时
WebSocket Origin websocket-client suppress_origin=True
关闭连接 vs 关闭 tab CDP.close() 只关闭 WebSocket;需要关闭浏览器页面时必须调用浏览器 target 关闭接口。采集只关闭本轮自动新建的商品页,复用的用户已有 tab 不关闭;③ 更新时程序自动新建的商品页成功/失败都关闭,成功提交且确认跳回商品列表页时关闭前等待 2 秒;③ 复用用户已有商品页时不关闭页面
前台激活 ①采集和⑥AI工场只读打开商品页时不主动 Page.bringToFront;新建 tab 尝试 Target.createTarget(background=true),不支持时退回普通新建。③更新真实提交每条任务都以前台方式新建或激活商品 tab,并执行 Page.bringToFront,保障上传、图片管理器刷新和拖拽排序稳定;后台态封面恢复逻辑仅保留给兼容直接调用,不作为正常③批量路径
SPA 就绪 不用 load 事件;轮询“标题输入框 + 图片 itembox + 上传输入框”三者都在
商品页错误 toast Shopee 错误提示使用 .eds-toasts / .eds-toast__content,可能很快隐藏或 display:none。打开商品页/等待 SPA 就绪前应注入 MutationObserver 或等价监听,把 toast 文本、outerHTML、当前 URL、时间、可见状态保存到页面缓存(如 window.__cmshopee_toasts);等待详情页关键元素超时时,再兜底读取当前 DOM 中的 toast。最近错误 toast 应优先成为 open_product 失败原因,并写入 DB 运行日志和本地脱敏诊断日志。只有明确商品失效/不存在/无权限类 toast 才驱动①阶段列显示“商品失效”;网络、CDP、未登录、页面超时、风控等其他失败仍显示“失败”

| 标题输入框 | XPath //input[@class='eds-input__input' and string-length(@modelvalue)>24] | | 写标题 | 原生 setter + 派发 input/change;value==modelvalue==新值 | | 读旧封面 | 第一张 itembox 的 img.src(susercontent CDN),下载到本地 | | AI工场原主图读取 | 复用 open_product(..., bring_to_front=False) 后台只读打开商品详情页,使用已验证 itembox 顺序读取全部主图 img.src 并返回 {index, src};不要求上传 input 之外的新选择器、不下载图片、不改标题/封面、不拖拽、不点击更新;本轮自动新建 tab 按采集规则关闭,复用用户已有 tab 不关闭 | | 上传输入框 | .shopee-image-manager__upload input[type=file];上传前先点击 .shopee-image-manager__upload 上传块以模拟人工选择图片入口,短暂等待后重新获取 input,再用 DOM.setFileInputFiles 传 Windows 路径并派发 input/change | | 上传成功 | 上传前先等图片管理器稳定。注意分两种状态:未满 9 张时,上传前要求图片 src 连续稳定、无 loading/blob、上传 input 存在且未禁用;满 9 张时,删除第一张之前只要求现有图片列表稳定,不得要求上传 input 可用,因为 Shopee 可能因满格隐藏/禁用上传入口;删除成功后再要求上传 input 恢复可用。上传后等新图 src 为 susercontent。若手动上传成功但自动上传一直转圈,优先检查是否绕过了上传块点击导致 Shopee 前端上传队列未完整初始化;代码应走“点击上传块 → 等待 → 重新取 input → DOM.setFileInputFiles”的人工等价路径。T-404 补丁后超时失败会返回 upload_state,区分仍在转圈(UPLOAD_STILL_PROCESSING)、图片上传错误(UPLOAD_PAGE_ERROR)、裁剪弹窗(UPLOAD_CROP_REQUIRED)和上传入口未恢复(UPLOAD_INPUT_NOT_READY);上传阶段只能把图片管理器内错误或图片/文件/上传相关 toast 归为封面上传错误,物流/备货等页面级校验错误不能阻断封面上传,应留到点击「更新」提交阶段处理;有1張重複的圖片 / 重複 / 重复 / duplicate 属于封面上传错误,必须立即失败并提示新封面与现有商品图片重复 | | 封面=第一位 | Input.dispatchMouseEvent 拖到第一位,落点 第一张.left - 0.30*宽 | | 换封面删除 | 更新封面统一先删当前第一张,不再只限满 9 张;先确认本地旧封面备份存在,再点第一张删除(.shopee-image-manager__icon--delete 或同类 delete 标记)并在可见 dialog/modal/popover 内点删除/确认按钮。关键顺序:删除前只等当前图片列表稳定,不检查上传 input;删除后不能只看数量减少,必须等图片管理器达到删除后数量、无 busy/blob、上传 input 恢复并短暂稳定,再重新获取 input 上传新图、确认取得 Shopee CDN 地址后拖到第一位;备份缺失则拒绝删除。已在 9 图测试商品 29671243750 上实测不提交流程,8 图商品也按同一替换语义删除第一张后再上传 | | 更新按钮 | 页面主更新按钮为 button.eds-button 中 <span>更新</span>;③ 批量确认后逐条点提交;禁用态(校验未过)记为失败。2026-06-29 实测点击后会弹 Shopee 站点侧确认框:可见 .eds-modal__content / .eds-modal__box,标题 確定您要更新商品嗎?,footer 中 立即優化 为次按钮,更新 为 eds-button--primary 主按钮;代码必须点击确认框内主按钮 更新 才算提交,不点 立即優化。2026-06-30 实测确认成功后会跳回 https://seller.shopee.tw/portal/product/list/all?operationSortBy=modified_time 商品列表页,代码需记录 post_update.url/redirected_to_list 作为提交后观测结果;若随后要关闭本轮自动新开 tab,必须先暂停 2 秒再关闭。判断优先级:跳转到 /portal/product/list/ 是强成功信号,应优先于残留/短暂 error toast;只有在未跳转列表页、无成功 toast,且错误 toast 持续存在时,才判 POST_UPDATE_ERROR。未处理确认框时不得认为已提交 | | 登录检测 | 重定向到登录页(含 accounts.shopee.tw/seller/login)或缺 SPC_ST/SPC_U → 未登录 |

高风险动作(删除线上封面、点更新提交、AI 图上线)先在测试商品验证。删除线上第一张封面前必须已有本地旧封面备份,不能在备份缺失时盲删线上图片。注意:本设计无逐条人工审核阶段、无常驻提交开关;③ 点击「开始更新」后必须弹窗确认,确认后才把当前筛选结果中的 AI 标题/封面提交线上。新图本地留档+回写 Excel 是事后追溯手段。

八、推荐开发顺序

  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。
  3. 导入采集(①):excel 导入、采集旧标题/旧封面、回写。
  4. AI 生成(②):ai 模块、提示词、对照预览。
  5. 更新蝦皮(③):对已生成任务弹窗批量确认后换标题+封面、提交、回写。
  6. 设置(⑤)+ 首次未配账号引导保护。

九、项目结构建议

cmshopee/
├── docs/
├── app/
│   ├── __init__.py
│   ├── __main__.py            # 支持 python -m app
│   ├── cdp.py                 # CDP 底座(由根目录 cdp.py 迁入)
│   ├── appconfig.py / db.py / excel.py / config.py / accounts.py / chrome.py
│   ├── editor.py / ai.py / prompts.py / gui.py / workers.py
│   ├── release_manifest.py   # 发布包文件清单、zip哈希与服务端元数据模板
│   ├── update_installer.py   # 自动升级安全下载、解压、manifest校验与同盘暂存
│   ├── updater_entry.py      # 独立更新器入口、事务根项目切换、journal与回滚
│   ├── gui/update_dialog.py  # 强制升级模态进度、下载worker与重启编排
│   ├── update_health.py      # 新版启动健康标记与失败版本熔断
├── main.py                    # GUI 启动入口:from app.gui import main
├── shopee待处理任务模板.xlsx   # 标准空 Excel 模板,可提交;业务填写后的副本不提交
├── data/                     # 用户本地数据根(整体 gitignore;打包更新时保留)
│   ├── config.json           # 应用配置(模型选择/生成参数/路径)
│   ├── config/ai_models.json # direct AI 模型清单(含密钥)
│   ├── config/cmhub.json     # cmhub API Key(含密钥)
│   ├── cmshopee.db           # SQLite(账号/任务/结果)
│   ├── chrome_user_data_dir/ # 各账号 Chrome 配置(含登录态)
│   ├── images/               # 旧封面/新封面本地图片
│   ├── title_prompt.txt      # 标题当前工作文本(启动回显)
│   └── prompts/
│       ├── title/<名称>.txt  # 标题提示词命名模板
│       ├── cover/<名称>.txt  # 封面提示词命名模板
│       └── image_studio/<名称>.txt # AI工场完整提示词模板
└── prototypes/               # 已验证原型/探查脚本(demo/set_*/get_title/cookies/inspect_images/grab/1.py)
                              # 逻辑待并入 app/editor.py 后清理;见 prototypes/README.md

data/、旧布局的 config.json、config/ai_models.json、config/cmhub.json、cmshopee.db、chrome_user_data_dir/、images/ 含配置/密钥/凭证/业务数据,必须 gitignore。shopee待处理任务模板.xlsx 是标准空模板,可以提交;运营填写后的 Excel 副本属于业务数据,不提交。

十、架构纪律

10.1 发布与自动升级边界

  • app/version.py 是应用版本唯一来源;app/release_manifest.py 生成 package-manifest.json 和 release-metadata.json,构建脚本不得自行维护另一套 hash 或路径规则。

  • 发布包格式固定为 cmshopee-portable-v1,入口为 cmshopee.exe,程序依赖集中在 _internal/。manifest 覆盖除自身外的所有程序文件,并记录规范化相对路径、字节数和 SHA-256。

  • 发布包只能包含程序根项目,data/ 和 .cmshopee-update/ 永远在替换边界之外。第一阶段预留签名字段,但 SHA-256 只负责传输完整性,不等同于发布者身份认证。

  • T-615 只提供可验证发布契约;启动门禁仍保持 T-544 的人工下载行为,直到后续下载、独立更新器、事务替换和失败熔断任务全部接入。

  • T-616 的下载暂存根固定为安装目录下 .cmshopee-update/,与 data/ 完全隔离。T-623 为兼容对象存储/CDN外链,临时允许任意公网 HTTP/HTTPS 域名或公网 IP 及跨域、跨协议重定向,但版本接口继续使用受信任 HTTPS;初始地址、每次跳转和最终地址均拒绝本机、内网及非 HTTP(S) 目标。远程zip必须通过声明大小、整包SHA-256、安全zip路径和包内manifest逐文件校验,才写 pending.json;HTTP 不降低任何安装校验,也不替换运行中程序文件。

  • T-617 的独立 cmshopee-updater.exe 必须先复制到系统临时目录运行,并等待主程序退出。替换粒度是manifest允许的程序根项目,旧根先整体移动到同盘backup,新根再整体移入;事务锁防止并发更新,journal记录每次移动,任一步失败逆序恢复。data/、更新管理目录与未知安装根项目永不进入替换清单。

  • T-618 在创建 MainWindow 前显示强制升级 QDialog;下载与校验只能在 QObject + QThread worker中执行,线程结束前保留引用,取消时等待part清理。独立更新器进程成功创建后才退出旧主程序;强制版本后续失败保持阻断,只有版本接口本身不可达/非法继续失败放行。

  • T-619 要求新版按事务写 process_started、main_window_ready、environment_blocked 健康标记。更新器只在主窗口就绪后认定升级成功并清理成功备份;无标记/早期退出/超时则事务回滚并按目标版本+包hash熔断。环境阻断保留新版,不把本地数据目录、配置或Chrome问题误判成坏发布包。

  • CDP 交互事实变化同步第七节。

  • 正式代码只放 app/ 包;根目录只保留 main.py、配置/数据目录、文档和原型目录,不新增正式业务模块。

  • app 包内模块优先用相对导入(如 from .cdp import CDP);根入口 main.py 用 from app.gui import main。

  • 存储边界:应用设置→data/config.json,账号/任务/结果→SQLite,图片→本地目录并记路径于 DB,登录态→user-data-dir;同一事实只存一处。

  • 别名是账号↔任务唯一关联键。

  • 密码与 AI Key 本地明文保存,保存/变更时弹窗提示;UI 打码、不外传、不写日志/导出;不自动登录。

  • AI 生成内容直接进入 ③ 更新候选;③ 批量确认后提交,新图本地留档 + 回写 Excel 以备追溯。

  • 批次删除是软删除:写 batches.deleted_at/deleted_reason,不物理删除 batches/tasks;默认 list_batches/list_tasks/get_task 与 ①/②/③ 页面和执行入口都排除已删除批次。

  • 高风险模块先单独验证,再接入流水线。

  • GUI 只通过 signal/slot 接收 worker 进度;禁止后台线程直接操作 Qt widget 或共享 SQLite connection。