Files
cmshoppe/docs/04-architecture.md
T
2026-07-04 15:13:15 +08:00

511 lines
45 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 架构设计
> 本文讲“怎么把技术栈搭起来”:模块职责、存储模型、流水线、CDP 已验证事实、开发顺序。
> 具体用了哪些库 / 平台,见 [技术栈](03-tech-stack.md)。
## 一、系统结构
Windows 本地桌面自动化工具,无后端服务,5 Tab GUI 驱动一条流水线。
```text
运营(人)
|
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、目录创建
├── accounts 账号 CRUD 服务:生成 slug/目录、启动登录、检测登录
├── chrome 按账号拼启动参数、启动/探测 Chrome、生成快捷方式
├── cdp CDP 客户端(连接、找/开 tab、执行 JS、拖拽)
├── editor 登录检测 / 采集旧标题旧封面 / 改标题 / 换封面 / 点更新
└── ai 文本生成(提示词+旧标题→新标题)/ 图像生成(提示词+旧封面→新封面)
|
v
Google Chrome(每账号独立 --user-data-dir + --remote-debugging-port) + AI 服务(direct 外部模型或 cmhub 网关)
|
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`。
- 核心模块统一放在正式代码包 `app/`:`appconfig.py`、`db.py`、`excel.py`、`config.py`、`accounts.py`、`chrome.py`、`editor.py`、`workers.py`、`ai.py`、`prompts.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 服务(文本+图像;direct 模式由 `config/ai_models.json` 配置,cmhub 模式由 `config.json` 的 `ai.cmhub` + `config/cmhub.json` 配置);`openpyxl`。
## 二、流水线(核心)
每个任务(Excel 行)依次走过 4 个业务阶段,状态字段 `stage` 贯穿全程:
```text
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)并存本地,关闭时 `new_cover_path=NULL`。**不设逐条人工审核阶段**,标题生成成功即可进入 ③;有新封面时 ③ 可在安全开关允许后换封面,没有新封面时 ③ 只改标题。
- **applied**:③ 点击「开始更新」后弹窗确认当前筛选范围和任务数量;确认后打开编辑页换标题+封面,逐条点「更新」提交线上,回写结果。
任意阶段失败 → `stage` 不前进、记 `error`、`result/skipped`,不影响其他任务。
## 三、职责划分
**GUI(5 Tab)**:见 [routes.md](routes.md)。只做交互与预览,不写业务逻辑;耗时操作走 PySide6 `QObject` worker + `QThread`,用 signal 回主线程刷新 UI。**首次未配账号、对应账号 Chrome 未启动或未登录时,① ③ 执行按钮禁用或执行前拦截,并提示去 ④;③ 不静默自动打开账号 Chrome。**
**核心模块**
- `appconfig`:读写 `config.json`(Chrome 路径、`chrome_user_data_dir` 根、图片目录、AI 配置、端口、DB 路径)。
- `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;支持 `direct` 模型清单和 `cmhub` 网关 backend;返回值保持标题字符串/本地 JPEG 路径)。
**存储(同一事实只存一处)**
- 应用配置(模型选择、生成参数、目录、Chrome 路径)→ `config.json`。
- AI 模型清单(direct 模式 url/模型/密钥/类型/连接超时)→ `config/ai_models.json`(API Key 本地明文保存,必须 gitignore,UI 打码显示)。
- cmhub 网关 Key → `config/cmhub.json`,schema `{ "api_key": "..." }`;`config.json` 只保存 Base URL、别名和超时,不保存 Key。
- 业务数据(账号、任务、各阶段结果)→ 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`
```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,
"backend": "direct",
"cmhub": {
"base_url": "",
"title_alias": "",
"image_alias": "",
"connect_timeout": 10,
"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",
"allow_real_submit": false,
"allow_cover_update": false,
"max_items_per_run": 1,
"close_success_tab": false,
"dry_run": false,
"parallel_accounts": false,
"max_parallel_accounts": 2
}
}
```
`ai` 段只放**选择 + 全局生成参数**:
- `backend`:`direct` / `cmhub`。全新配置和旧配置缺字段时均为 `direct`,避免未配置 cmhub 时破坏既有直连生成;只有用户在⑤显式切换并配置完整后才走 cmhub。
- `cmhub`:cmhub 网关配置,`base_url` 为网关根地址,`title_alias` / `image_alias` 为 `GET /api/v1/models` 发现的能力别名,`connect_timeout` 为连接超时;API Key 不在此处保存。
- `default_text_model` / `default_image_model`:direct 模式下引用 `ai_models.json` 里的模型名(标题用文本模型、封面用图像模型);cmhub 模式不读取这些模型定义。
- `generate_cover`:②「开始生成」时是否调用图片模型生成新封面;默认 `false`,避免用户无意产生图片生成成本。该字段只控制 AI 生成阶段,不等同于 ③ 的 `allow_cover_update`。
- `resolution`:当前分辨率,下拉 `512 / 1k / 2k / 4k`。
- `resolution_timeouts`:分辨率 → **等待大模型返回超时(秒)** 的映射;用户选分辨率即自动套用,不单独填。
- direct 模式模型本身的定义(url/key/类型/连接超时…)在 `config/ai_models.json`,见 5.1b。
- 密钥不在 `config.json`:direct 模式每个模型的 `api_key` 存于 `config/ai_models.json`;cmhub 模式 API Key 存于 `config/cmhub.json`。两者均本地明文保存、保存/变更时弹窗提示、UI 打码、gitignore、不入日志/导出。
`shopee_update` 段放**真实更新前的安全开关**:
- `test_item_id`:历史/调试兼容字段;默认 `51100639510`。普通正式更新不再以该字段限制商品 ID,也不因当前筛选结果包含非测试商品而阻断;后续如需要调试模式,可单独启用测试商品限制。
- `allow_real_submit`:是否允许 ③ 创建 `ApplyWorker` 并点击「更新」提交线上;默认 `false`,未开启时 ③ 在确认弹窗前阻断。
- `allow_cover_update`:是否允许本次更新包含新封面路径;默认 `false`,未开启时只允许标题更新任务。
- `max_items_per_run`:每批最大更新任务数;默认 `1`,当前筛选结果超过该值时自动分批,不再按总数阻断。
- `close_success_tab`:成功提交后是否关闭本轮程序自动新开的商品编辑页;默认 `false`。只关闭 `open_product()` 本轮新建且已提交成功的 tab,失败任务和用户原本打开的 tab 保留。Shopee 确认成功并跳回商品列表页后,关闭前等待 2 秒,让列表页跳转和页面状态稳定。
- `dry_run`:内部兼容字段;普通用户界面不展示该开关,③「检查本轮更新」按钮触发检查模式,只写运行日志,不打开 Shopee、不点击「更新」、不改任务状态;默认 `false`。
- `parallel_accounts`:是否按账号并行执行③真实更新;默认 `false`,即保持串行。
- `max_parallel_accounts`:最多同时执行的账号数;同一账号内仍按任务串行,默认 `2`。
该段不是替代 ③ 确认弹窗的常驻授权;③ 仍必须弹窗确认,用户点是后才执行。`dry_run=true` 时不会真实提交;`dry_run=false` 时仍必须先通过真实更新安全开关检查。普通正式更新的安全检查不读取 `test_item_id` 做阻断。
### 5.1b AI 模型清单 `config/ai_models.json`
模型定义清单("有哪些模型"),与 `config.json` 的 `ai` 段("选了哪个 + 全局参数")职责分开。该文件只用于 `backend=direct`;`backend=cmhub` 时生文/生图使用 cmhub 别名,不读取此文件。
```json
{
"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 `config/cmhub.json`
```json
{ "api_key": "sk_cmhub_xxx" }
```
- 文件必须 gitignore,不提交;UI 展示打码。
- `appconfig.load_cmhub_config()` 缺文件时返回空 Key,不自动启用 cmhub。
- `backend=cmhub` 但 Base URL、API Key 或别名缺失时,`app/ai.py` 抛 `CMHubError(code="cmhub_not_configured")`,提示去⑤设置配置,不静默回退 direct。
### 5.2 SQLite `cmshopee.db`
```sql
-- 批次(一次导入动作)
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, -- 新封面本地图片路径
-- 应用
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):
```sql
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 文件(开发调试)**:写入 `logs/cmshopee.log` 或按日期滚动文件,保存脱敏后的 traceback、异常类型、步骤、耗时和必要上下文。`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 事件保存最后步骤和简短错误,本地 `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 运行日志和本地 `logs/cmshopee.log` 都写脱敏错误。
3. **T-505 已扩展全流程**:Excel 导入创建 `run_type=import`,记录选中文件、缺列、脏行、入库统计;Excel 回写创建 `run_type=write_back`,记录文件写入、文件锁/保存异常和行数;③ 更新shopee 的 `run_type=apply` 覆盖安全/账号预检、Chrome/CDP 检查、登录检测、打开商品页、改标题、换封面、点更新、写库;④「启动登录」创建 `run_type=chrome_launch`,④「检测登录」创建 `run_type=login_check`;⑤ AI 模型测试连接创建 `run_type=ai_model_test`。失败时本地 `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。
原则:数据库日志给运营和 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` 是可选结果,只有②勾选生成封面且图片成功时才写入本地新封面路径;不设逐条确认阶段,标题生成成功即可进入 ③。
- `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 本地图片目录
```text
images/<batch_id>/<slug>/<task_id>_<item_id>_old.<ext> # 采集下载的旧封面
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` 路径继续有效,不做强制迁移;新采集/新生成才使用按批次细分的目录。
## 六、关键流程细节
### 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。
- 若商品 ID 失效、无权限或店铺不匹配导致商品编辑页无法就绪,`open_product` 必须读取/捕获 Shopee toast,把最近错误文案写入采集失败原因和诊断日志,不能只返回泛化超时。①列表只在明确捕获商品失效类 toast 时把“阶段”显示为“商品失效”;底层 `stage` 不新增中文值。若这个失败发生在程序自动新建的商品 tab 内,`open_product` 要关闭该 tab;复用用户已有 tab 不关闭。
- 旧封面:取第一张 itembox 的 `img.src`(CDN 链接),下载到 `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=direct` 时使用 `default_text_model`;`backend=cmhub` 时调用 `POST /api/v1/generate/title` 并使用 `title_alias`。
2. **若②勾选「生成封面图片(成本较高)」**:接着并发生成图片,线程池大小 = `image_concurrency`,调 `gen_cover(封面提示词, old_cover_path, resolution, jpg_quality)` → 新图存 `images/<batch_id>/<slug>/<task_id>_<item_id>_new.jpg`。`backend=direct` 时使用 `default_image_model`;`backend=cmhub` 时调用 `POST /api/v1/generate/image`,拿 `image_url` 后立即安全下载并转本地 JPEG。
- 连接超时取该模型 `connect_timeout_seconds`;**返回超时取 `resolution_timeouts[resolution]`**(512→180/1k→240/2k→360/4k→600)。
3. **若未勾选生成封面**:标题成功后立即写 `new_title`,`new_cover_path=NULL`,不渲染封面提示词、不调用 `gen_cover()`、不创建本地新封面文件。
- 失败重试:每次调用失败按 `retry` 次重试,仍失败则记 error(不阻塞其余)。
- 标题-only 模式在标题成功后立即 `set_generated(task_id, new_title, NULL)` 写库;标题+封面模式在图片成功后 `set_generated(task_id, new_title, new_cover_path)` 写库。两种模式都实时落库,停止或崩溃不丢已生成结果。
- **「停止」**:取消未开始的任务,正在跑的少量完成或中断;停止后可再次「开始生成」对剩余继续。
- 进度:标题和图片两条进度分开显示;未勾选生成封面时图片进度显示本轮未生成/0 张,并在运行日志写明“本轮仅生成标题”。
- 生成后 stage=generated;**不设逐条人工审核阶段**。若未生成封面,任务仍可进入③并只更新标题;若后续需要封面,用户可重置生成结果或重新生成该任务。双击任务弹窗查看新旧封面(纯查看,无新封面时显示为空);T-404a 已提供对当前选中单条的「重置生成结果」,确认后只清本地 AI 结果并退回 collected 供重新生成,默认不删除本地新封面文件;新标题直接用 AI 输出(不可编辑)。
- 并发数、重试、分辨率、jpg 质量、模型/Key 均来自 ⑤ 设置(`config.json` 的 `ai` 段)。
提示词管理:
- **标题提示词**:单个文本,「保存」写入 `title_prompt.txt`;软件启动时加载该文件回显到输入框(缺失则空)。
- **封面提示词**:多模板。下拉选模板(读 `prompts/cover/*.txt`),图标工具栏 新建/保存/另存为/重命名/删除;重名校验、删除二次确认、删空给默认。
- **变量**:封面提示词支持占位符 `{旧标题}`、`{新标题}`、`{商品id}`、`{店铺}`,生成前用该任务真实值替换(`render_prompt`)。「插入标题」= 在光标处插入 `{新标题}`;「预览」= 用某条任务的值替换变量后展示,确认实际发送给 AI 的内容。
### 6.3 应用更新(③ Tab)
- ③ 顶部筛选确定本次作用范围;点击「开始更新」后弹窗展示筛选条件、任务数量和“将提交线上”的风险提示。
- ③ 提供「检查本轮更新」按钮:只读取当前筛选结果和写运行日志,不打开 Shopee、不提交、不改任务状态;检查汇总展示总数、店铺分布、每批最大条数、预计批次数、更新字段和略过原因。
- 弹确认前先读取 `config.json` 的 `shopee_update`:未开启 `allow_real_submit`,或任务含新封面但未开启 `allow_cover_update` 时,直接弹警告阻断,不创建真实更新 `ApplyWorker`。普通正式更新不再检查 `test_item_id`,当前筛选结果可以包含多个真实商品 ID。`max_items_per_run` 作为每批最大任务数,当前筛选总数超过该值时自动分批,不再按总数阻断。
- 用户点「是/确认」才开始批量更新;点「否/取消」不执行、不改库。
- 真实更新前必须做账号就绪预检:按当前筛选结果汇总需要的账号;无账号、账号 Chrome 未启动、CDP 端口不可访问、未登录或本轮账号端口冲突时,整体返回 `blocked` 并由 GUI 弹窗列出账号/原因、引导去④账号管理。预检不通过时不创建商品编辑页、不调用 `editor.apply_task()`、不写失败状态、不自动调用「启动登录」或静默打开 Chrome。
- 对确认后的**已生成(generated)任务**:`open_product` → `change_title(new_title)`(如有)→ `replace_cover(new_cover_path)`(如有)→ `click_update` 提交。标题-only 任务没有 `new_cover_path` 时跳过封面更新,也不需要开启 ⑤「允许更新封面」。
- `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` 非空且文件存在),再删除当前线上第一张图、等待图片管理器稳定、上传新图并拖到第一位。缺失备份时不删除线上第一张图,直接返回明确错误,要求先回到①采集旧封面或修复本地备份。
- 默认串行、单条失败继续;⑤ 开启 `parallel_accounts` 后按账号分组并行,不同账号可同时跑,同一账号内仍串行。真实更新前检查本轮账号 `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`,必须提示线上已提交过、本地重置不回滚 Shopee、重复更新会再次提交,并保留 committed 历史事实/运行日志。
- 若 `close_success_tab=true`,且商品页是本轮程序自动新建、并已成功提交,则提交后关闭该商品编辑页;若确认后已跳回商品列表页,关闭前等待 2 秒;进入编辑页后的失败任务和复用的用户已有 tab 不关闭;`open_product` 内部打开失败的新建 tab 要关闭。
### 6.4 登录检测
- 无 Shopee tab 时打开卖家中心根地址 `https://<region_host>/`(默认 `https://seller.shopee.tw/`),重定向到登录页或缺会话 Cookie(`SPC_ST`/`SPC_U`)→ 未登录;不自动登录,提示人工登录。
- ④「启动登录」只负责打开该账号独立 user-data-dir + CDP 端口的 Chrome,供用户人工登录;「检测登录」只验证当前 Chrome/CDP/会话 Cookie 是否可用。①/③ 的预检只检测,不自动启动缺失浏览器。若商品详情页或卖家中心重定向到 `accounts.shopee.tw/seller/login`,必须按登录页处理,返回 `LOGIN_PAGE` 并在 GUI 显示未登录。
## 七、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 不关闭;③ 仅在设置 `close_success_tab=true`、成功提交、且 tab 为本轮自动新建时关闭;确认成功跳回商品列表页时,关闭前等待 2 秒 |
| 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),下载到本地 |
| 上传输入框 | `.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 图上线)先在测试商品验证。删除线上第一张封面前必须已有本地旧封面备份,不能在备份缺失时盲删线上图片。注意:本设计无逐条人工审核阶段、无常驻提交开关;③ 点击「开始更新」后必须先通过 `shopee_update` 安全开关,再弹窗确认,确认后才把当前筛选结果中的 AI 标题/封面提交线上。新图本地留档+回写 Excel 是事后追溯手段。
## 八、推荐开发顺序
1. **地基**:先建 `app/` 包、`app/cdp.py`、`app/__main__.py`、根目录 `main.py`;再做 `app/editor.py`(含采集)、`app/appconfig.py`+`config.json`、`app/db.py`、`.gitignore`。
2. **账号与启动**(④):`config` 建目录、`chrome` 启动器/快捷方式、登录保活与检测、账号 CRUD。
3. **导入采集**(①):`excel` 导入、采集旧标题/旧封面、回写。
4. **AI 生成**(②):`ai` 模块、提示词、对照预览。
5. **更新 shopee**(③):对已生成任务弹窗批量确认后换标题+封面、提交、回写。
6. **设置**(⑤)+ 首次未配账号引导保护。
## 九、项目结构建议
```text
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
├── main.py # GUI 启动入口:from app.gui import main
├── shopee待处理任务模板.xlsx # 标准空 Excel 模板,可提交;业务填写后的副本不提交
├── config.json # 应用配置(模型选择/生成参数/路径,gitignore)
├── config/ai_models.json # direct AI 模型清单(含密钥,必须 gitignore)
├── config/cmhub.json # cmhub API Key(含密钥,必须 gitignore)
├── cmshopee.db # SQLite(账号/任务/结果,gitignore)
├── chrome_user_data_dir/ # 各账号 Chrome 配置(含登录态,gitignore)
├── images/ # 旧封面/新封面本地图片(gitignore)
├── title_prompt.txt # 标题提示词(单文件,启动回显)
├── prompts/cover/<名称>.txt # 封面提示词模板(多个)
└── prototypes/ # 已验证原型/探查脚本(demo/set_*/get_title/cookies/inspect_images/grab/1.py)
# 逻辑待并入 app/editor.py 后清理;见 prototypes/README.md
```
> `config.json`、`config/ai_models.json`、`config/cmhub.json`、`cmshopee.db`、`chrome_user_data_dir/`、`images/` 含配置/密钥/凭证/业务数据,必须 gitignore。`shopee待处理任务模板.xlsx` 是标准空模板,可以提交;运营填写后的 Excel 副本属于业务数据,不提交。
## 十、架构纪律
- CDP 交互事实变化同步第七节。
- 正式代码只放 `app/` 包;根目录只保留 `main.py`、配置/数据目录、文档和原型目录,不新增正式业务模块。
- `app` 包内模块优先用相对导入(如 `from .cdp import CDP`);根入口 `main.py` 用 `from app.gui import main`。
- 存储边界:应用设置→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。