Files
cmshoppe/docs/04-architecture.md
T
chengma 5c6fd0ff98 docs: 明确任务领取与导入安全规则
统一任务领取规则,current-state 只能展示按 06-tasks 顺序计算出的当前任务,当前下一个任务固定为 T-001。

明确 tests/ 与 unittest discover 的启用时机:T-006 前不因 tests 缺失判失败,T-006 后纯逻辑改动必须补测并运行。

补齐敏感文件和目录清单:config.json、config/ai_models.json、cmshopee.db、chrome_user_data_dir/、images/ 均须 gitignore 且不得提交。

定稿 Excel 导入容错策略:缺必需列拒绝整文件并记录 file_errors,单行脏数据逐行跳过并计入 invalid。
2026-06-26 16:37:58 +08:00

22 KiB
Raw Blame History

架构设计

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

一、系统结构

Windows 本地桌面自动化工具,无后端服务,5 Tab GUI 驱动一条流水线。

运营(人)
  |
  v
GUI(PySide6 QTabWidget,5 Tab)
  ① 导入采集  ② AI生成  ③ 更新shopee  ④ 账号管理  ⑤ 设置
  |
  v
核心模块(Python)
  ├── appconfig  读应用配置 config.json(Chrome 路径、目录根、AI 配置、端口范围…)
  ├── db         SQLite 读写:账号、任务、各阶段结果(cmshopee.db)
  ├── excel      openpyxl 导入输入列 / 回写输出列到原 Excel
  ├── config     账号 ↔ user-data-dir 绑定、slug、目录创建
  ├── chrome     按账号拼启动参数、启动/探测 Chrome、生成快捷方式
  ├── cdp        CDP 客户端(连接、找/开 tab、执行 JS、拖拽)
  ├── editor     登录检测 / 采集旧标题旧封面 / 改标题 / 换封面 / 点更新
  └── ai         文本生成(提示词+旧标题→新标题)/ 图像生成(提示词+旧封面→新封面)
  |
  v
Google Chrome(每账号独立 --user-data-dir + --remote-debugging-port) + AI 服务(外部)
  |
  v
Shopee 卖家中心页面 / 本地图片目录

真实组件:

  • GUI 入口:gui.py(待建,PySide6 + QMainWindow + QTabWidget,5 Tab)。
  • 核心模块:appconfig.py、db.py、excel.py、config.py、chrome.py、editor.py、ai.py(待建);CDP 底座 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 服务(文本+图像,服务商待定);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 用提示词+旧标题生成新标题;用提示词+旧封面生成新封面(image-to-image),新图存本地。不设逐条人工审核阶段,生成完即可进入 ③(新图留档本地 + 回写 Excel 供事后追溯)。
  • applied:③ 点击「开始更新」后弹窗确认当前筛选范围和任务数量;确认后打开编辑页换标题+封面,逐条点「更新」提交线上,回写结果。

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

三、职责划分

GUI(5 Tab):见 routes.md。只做交互与预览,不写业务逻辑;耗时操作走 PySide6 QObject worker + QThread,用 signal 回主线程刷新 UI。首次未配账号/未登录时,① ③ 执行按钮禁用并提示去 ④。

核心模块

  • appconfig:读写 config.json(Chrome 路径、chrome_user_data_dir 根、图片目录、AI 配置、端口、DB 路径)。
  • db:SQLite 读写账号、任务、各阶段结果;建表/迁移。
  • excel:openpyxl 读输入列、把输出列回写原 Excel(处理文件锁)。
  • config:账号 ↔ user-data-dir 绑定;slug;目录创建。
  • chrome:拼接启动命令、启动、探测端口、(可选)生成快捷方式。
  • cdp:连接调试端口、找/开 tab、执行 JS、拖拽、注入文件。
  • editor:登录检测、采集(读旧标题、下载旧封面)、改标题、换封面、点更新。
  • ai:gen_title(prompt, old_title)、gen_cover(prompt, old_cover_path)(外部 AI;服务商待定)。

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

  • 应用配置(模型选择、生成参数、目录、Chrome 路径)→ config.json。
  • AI 模型清单(url/模型/密钥/类型/连接超时)→ config/ai_models.json(API Key 本地明文保存,必须 gitignore,UI 打码显示)。
  • 业务数据(账号、任务、各阶段结果)→ SQLite cmshopee.db。
  • 图片(采集的旧封面、AI 生成的新封面)→ 本地图片目录(路径记在 DB)。
  • 提示词 → 标题提示词存单文件 title_prompt.txt;封面提示词存多模板 prompts/cover/<名称>.txt。
  • 登录态 → 各账号 chrome_user_data_dir/<slug>/。

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

采用每账号独立 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,参数写在「目标」字段)。

五、数据模型

5.1 应用配置 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",
    "title_concurrency": 4,
    "image_concurrency": 4,
    "retry": 2,
    "jpg_quality": 90,
    "resolution": "1k",
    "resolution_timeouts": { "512": 180, "1k": 240, "2k": 360, "4k": 600 }
  }
}

ai 段只放选择 + 全局生成参数:

  • default_text_model / default_image_model:引用 ai_models.json 里的模型名(标题用文本模型、封面用图像模型)。
  • resolution:当前分辨率,下拉 512 / 1k / 2k / 4k。
  • resolution_timeouts:分辨率 → 等待大模型返回超时(秒) 的映射;用户选分辨率即自动套用,不单独填。
  • 模型本身的定义(url/key/类型/连接超时…)在 config/ai_models.json,见 5.1b。
  • 密钥不在 config.json:每个模型的 api_key 存于 config/ai_models.json,本地明文保存、UI 打码、gitignore、不入日志。

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

模型定义清单("有哪些模型"),与 config.json 的 ai 段("选了哪个 + 全局参数")职责分开。

{
  "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 本地明文保存、打码显示。

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
);

-- 账号(④ 账号管理)
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,                   -- 新封面本地图片路径
  -- 应用
  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);

关键事实:

  • 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 生成结果(输出),图片落本地;不设逐条确认阶段,生成即可进入 ③。
  • 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 账号名(展示) 输入
别名 关联 accounts.alias(匹配键) 输入
商品id item_id 输入
旧标题 采集到的旧标题 输出,回写
旧封面图片路径 采集到的旧封面本地路径 输出,回写
新标题 AI 生成的新标题 输出,回写
新封面图片路径 AI 生成的新封面本地路径 输出,回写
更新状态 处理结果(成功/失败/跳过+原因) 输出,回写
  • 输入列(账号名/别名/商品id)运营填;其余为程序各阶段输出,回写到原 Excel(被锁→提示重试/另存副本)。
  • 别名以此列为匹配权威,不解析文件名。
  • 导入容错:缺少必需列(别名/商品id)时拒绝整个文件,记入 file_errors,不导入该文件任何行。
  • 脏行处理:单行缺别名/商品id、商品id 格式错误等逐行跳过,计入 invalid,不阻塞同文件其他有效行。

5.4 本地图片目录

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

路径记入 DB;上传新封面用本地 <item_id>_new 文件(Windows 绝对路径传 setFileInputFiles)。

六、关键流程细节

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)。
  • 旧封面:取第一张 itembox 的 img.src(CDN 链接),下载到 images/<slug>/<item_id>_old。
  • 写 old_title/old_cover_path、stage=collected;批量回写 Excel 旧字段。

6.2 AI 生成(② Tab)

单个「开始生成」按钮,两段式、各自并发(标题快、图片慢,分开并发更高效):

  1. 并发生成标题:线程池大小 = title_concurrency,用 default_text_model 调 gen_title(标题提示词, old_title) → new_title。
  2. 接着并发生成图片:线程池大小 = image_concurrency,用 default_image_model 调 gen_cover(封面提示词, old_cover_path, resolution, jpg_quality) → 新图存 images/<slug>/<item_id>_new.jpg。
    • 连接超时取该模型 connect_timeout_seconds;返回超时取 resolution_timeouts[resolution](512→180/1k→240/2k→360/4k→600)。
  • 失败重试:每次调用失败按 retry 次重试,仍失败则记 error(不阻塞其余)。
  • 每条/每张完成立即 set_generated 写库(实时落库,停止或崩溃不丢已生成的)。
  • 「停止」:取消未开始的任务,正在跑的少量完成或中断;停止后可再次「开始生成」对剩余继续。
  • 进度:标题 x/n · 封面 x/n · 失败 n。
  • 生成后 stage=generated;不设逐条人工审核阶段。双击任务弹窗查看新旧封面(纯查看),可选对某行 重生成;新标题直接用 AI 输出(不可编辑)。
  • 并发数、重试、分辨率、jpg 质量、模型/Key 均来自 ⑤ 设置(config.json 的 ai 段)。

提示词管理:

  • 标题提示词:单个文本,「保存」写入 title_prompt.txt;软件启动时加载该文件回显到输入框(缺失则空)。
  • 封面提示词:多模板。下拉选模板(读 prompts/cover/*.txt),图标工具栏 新建/保存/另存为/重命名/删除;重名校验、删除二次确认、删空给默认。
  • 变量:封面提示词支持占位符 {旧标题}、{新标题}、{商品id}、{店铺},生成前用该任务真实值替换(render_prompt)。「插入标题」= 在光标处插入 {新标题};「预览」= 用某条任务的值替换变量后展示,确认实际发送给 AI 的内容。

6.3 应用更新(③ Tab)

  • ③ 顶部筛选确定本次作用范围;点击「开始更新」后弹窗展示筛选条件、任务数量和“将提交线上”的风险提示。
  • 用户点「是/确认」才开始批量更新;点「否/取消」不执行、不改库。
  • 对确认后的已生成(generated)任务:open_product → change_title(new_title)(如有)→ replace_cover(new_cover_path)(如有)→ click_update 提交。
  • 串行、单条失败继续;每条立即写 SQLite;全部完成回写 Excel(新字段+状态)+ 弹窗汇总。

6.4 登录检测

  • 打开卖家中心,重定向到登录页或缺会话 Cookie(SPC_ST/SPC_U)→ 未登录;不自动登录,提示人工登录。

七、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
SPA 就绪 不用 load 事件;轮询“标题输入框 + 图片 itembox + 上传输入框”三者都在
标题输入框 XPath //input[@class='eds-input__input' and string-length(@modelvalue)>24]
写标题 原生 setter + 派发 input/change;value==modelvalue==新值
读旧封面 第一张 itembox 的 img.src(susercontent CDN),下载到本地
上传输入框 .shopee-image-manager__upload input[type=file];DOM.setFileInputFiles 传 Windows 路径
上传成功 张数 +1 且新图 src 为 susercontent
封面=第一位 Input.dispatchMouseEvent 拖到第一位,落点 第一张.left - 0.30*宽
满 9 张 上限 9;换封面先删第一张(.shopee-image-manager__icon--delete,确认框待实测)
更新按钮 button.eds-button 中 <span>更新</span>;③ 批量确认后逐条点提交;禁用态(校验未过)记为失败
登录检测 重定向到登录页或缺 SPC_ST → 未登录

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

八、推荐开发顺序

  1. 地基:editor.py(含采集)、appconfig.py+config.json、db.py、.gitignore。
  2. 账号与启动(④):config 建目录、chrome 启动器/快捷方式、登录保活与检测、账号 CRUD。
  3. 导入采集(①):excel 导入、采集旧标题/旧封面、回写。
  4. AI 生成(②):ai 模块、提示词、对照预览。
  5. 更新 shopee(③):对已生成任务弹窗批量确认后换标题+封面、提交、回写。
  6. 设置(⑤)+ 首次未配账号引导保护。

九、项目结构建议

cmshopee/
├── docs/
├── config.json               # 应用配置(模型选择/生成参数/路径,gitignore)
├── config/ai_models.json     # AI 模型清单(含密钥,必须 gitignore)
├── cmshopee.db               # SQLite(账号/任务/结果,gitignore)
├── chrome_user_data_dir/     # 各账号 Chrome 配置(含登录态,gitignore)
├── images/                   # 旧封面/新封面本地图片(gitignore)
├── title_prompt.txt          # 标题提示词(单文件,启动回显)
├── prompts/cover/<名称>.txt  # 封面提示词模板(多个)
├── appconfig.py / db.py / excel.py / config.py / chrome.py / editor.py / ai.py / prompts.py / gui.py   # 待建
├── workers.py                 # PySide6 worker/QThread 编排(可选拆分)
├── cdp.py                    # CDP 底座(正式模块,已有)
└── prototypes/               # 已验证原型/探查脚本(demo/set_*/get_title/cookies/inspect_images/grab/1.py)
                              # 逻辑待并入 editor.py 后清理;见 prototypes/README.md

config.json、config/ai_models.json、cmshopee.db、chrome_user_data_dir/、images/ 含密钥/凭证/业务数据,必须 gitignore。

十、架构纪律

  • CDP 交互事实变化同步第七节。
  • 存储边界:应用设置→config.json,账号/任务/结果→SQLite,图片→本地目录并记路径于 DB,登录态→user-data-dir;同一事实只存一处。
  • 别名是账号↔任务唯一关联键。
  • 密码与 AI Key 本地明文保存、UI 打码、不外传、不写日志;不自动登录。
  • AI 生成内容直接进入 ③ 更新候选;③ 批量确认后提交,新图本地留档 + 回写 Excel 以备追溯。
  • 高风险模块先单独验证,再接入流水线。
  • GUI 只通过 signal/slot 接收 worker 进度;禁止后台线程直接操作 Qt widget 或共享 SQLite connection。