530 lines
54 KiB
Markdown
530 lines
54 KiB
Markdown
# 架构设计
|
||
|
||
> 本文讲“怎么把技术栈搭起来”:模块职责、存储模型、流水线、CDP 已验证事实、开发顺序。
|
||
> 具体用了哪些库 / 平台,见 [技术栈](03-tech-stack.md)。
|
||
|
||
## 一、系统结构
|
||
|
||
Windows 本地桌面自动化工具,无后端服务,5 Tab GUI 驱动一条流水线。
|
||
|
||
```text
|
||
运营(人)
|
||
|
|
||
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 文本生成(提示词+旧标题→新标题)/ 图像生成(提示词+旧封面→新封面)
|
||
|
|
||
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`。
|
||
- 核心模块统一放在正式代码包 `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 服务(文本+图像;普通产品默认 cmhub 网关,由 `data/config.json` 的 `ai.cmhub` + `data/config/cmhub.json` 配置;direct 直连模型清单仅作为内部兼容/手工回滚路径保留);`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 按②「生成内容」下拉生成新标题和/或新封面;选择只生成标题时 `new_cover_path=NULL`,选择只生成封面时要求任务已有 `new_title`,选择生成标题和封面时可按缺失组件增量补齐。**不设逐条人工审核阶段**,生成成功即可进入 ③;③ 再按「更新内容」下拉决定只更新标题、只更新封面或更新图文。
|
||
- **applied**:③ 点击「开始更新」后弹窗确认当前筛选范围和任务数量;确认后打开编辑页换标题+封面,逐条点「更新」提交线上,回写结果。
|
||
|
||
任意阶段失败 → `stage` 不前进、记 `error`、`result/skipped`,不影响其他任务。
|
||
|
||
## 三、职责划分
|
||
|
||
**GUI(5 Tab)**:见 [routes.md](routes.md)。只做交互与预览,不写业务逻辑;耗时操作走 PySide6 `QObject` worker + `QThread`,用 signal 回主线程刷新 UI。**① 采集点击后会为本轮匹配到的账号自动确保 Chrome 就绪:已打开则复用,未打开才启动;随后只检测登录态,未登录账号的任务跳过并汇总提示去 ④人工登录。③ 更新蝦皮仍是线上提交高风险链路:执行前只检测账号 Chrome/CDP/登录态,不自动启动缺失账号 Chrome。**
|
||
|
||
**核心模块**
|
||
|
||
- `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/cover/<名称>.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`
|
||
|
||
```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",
|
||
"allow_real_submit": false,
|
||
"allow_cover_update": false,
|
||
"update_mode": "title",
|
||
"max_items_per_run": 1,
|
||
"close_success_tab": false,
|
||
"dry_run": false,
|
||
"parallel_accounts": false,
|
||
"max_parallel_accounts": 2
|
||
}
|
||
}
|
||
```
|
||
|
||
`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 不在此处保存。
|
||
- `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,也不因当前筛选结果包含非测试商品而阻断;后续如需要调试模式,可单独启用测试商品限制。
|
||
- `allow_real_submit`:是否允许 ③ 创建 `ApplyWorker` 并点击「更新」提交线上;默认 `false`,未开启时 ③ 在确认弹窗前阻断。
|
||
- `update_mode`:③「更新内容」下拉的主字段,取值 `title` / `cover` / `title_cover`,分别表示只更新标题、只更新封面、更新标题和封面;默认 `title`。
|
||
- `allow_cover_update`:旧兼容字段;保存配置时仍写回,值由 `update_mode` 是否包含封面推导。⑤设置页不再展示「允许更新封面」,封面是否参与本轮真实更新由③左下角「更新内容」下拉决定。
|
||
- `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 模型清单 `data/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 `data/config/cmhub.json`
|
||
|
||
```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`。
|
||
|
||
### 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, -- 新封面本地图片路径
|
||
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):
|
||
|
||
```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 文件(开发调试)**:写入 `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` 是可选结果,只有②勾选生成封面且图片成功时才写入本地新封面路径;不设逐条确认阶段,标题生成成功即可进入 ③。
|
||
- `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
|
||
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` 且缺新封面的任务;选择 `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`。
|
||
- 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, NULL)` 写库;只生成封面模式不调用生文、不覆盖已有标题;生成标题和封面模式在封面成功后 `set_generated(task_id, new_title, new_cover_path)` 写库。三种模式都实时落库,停止或崩溃不丢已生成结果。
|
||
- **「停止」**:取消未开始的任务,正在跑的少量完成或中断;停止后可再次「开始生成」对剩余继续。
|
||
- 进度:标题和图片两条进度分开显示;只生成标题时图片进度显示本轮未生成/0 张,并在运行日志写明本轮生成内容。
|
||
- 生成后 stage=generated;**不设逐条人工审核阶段**。若未生成封面,任务仍可进入③并选择只更新标题;若后续需要封面,用户可在②选择只生成封面或生成标题和封面补齐。双击任务弹窗查看新旧封面(纯查看,无新封面时显示为空);T-404a 已提供对当前选中单条的「重置生成结果」,确认后只清本地 AI 结果并退回 collected 供重新生成,默认不删除本地新封面文件;新标题直接用 AI 输出(不可编辑)。
|
||
- 并发数、重试、分辨率、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”。
|
||
|
||
提示词管理:
|
||
|
||
- **标题提示词**:单个文本,「保存」写入 `data/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`:未开启 `allow_real_submit` 时,直接弹警告阻断,不创建真实更新 `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` 提交。选择 `只更新标题` 时即使任务有 `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` 非空且文件存在),再删除当前线上第一张图、等待图片管理器稳定、上传新图并拖到第一位。缺失备份时不删除线上第一张图,直接返回明确错误,要求先回到①采集旧封面或修复本地备份。
|
||
- 默认串行、单条失败继续;⑤ 开启 `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`,必须提示线上已提交过、本地重置不回滚蝦皮、重复更新会再次提交,并保留 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 并打开/激活卖家中心登录 tab,不再新开 Chrome;若端口未响应,才启动 Chrome。「检测登录」只验证当前 Chrome/CDP/会话 Cookie 是否可用。① 采集的预检会复用同一幂等启动能力,自动确保本轮匹配账号 Chrome 就绪但不自动登录;③ 更新的预检只检测,不自动启动缺失浏览器。若商品详情页或卖家中心重定向到 `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 秒 |
|
||
| 前台激活 | ①采集只读打开商品页时不主动 `Page.bringToFront`;新建 tab 尝试 `Target.createTarget(background=true)`,不支持时退回普通新建。③更新真实提交仍需要 UI 交互稳定性,但为降低抢焦点,本轮每账号只在首条任务主动前台一次;后台态封面上传/拖拽遇到疑似遮挡节流失败时,再提前台做一次非破坏性安全恢复,不完整重跑删图上传流程 |
|
||
| 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`+`data/config.json`、`app/db.py`、`.gitignore`。
|
||
2. **账号与启动**(④):`config` 建目录、`chrome` 启动器/快捷方式、登录保活与检测、账号 CRUD。
|
||
3. **导入采集**(①):`excel` 导入、采集旧标题/旧封面、回写。
|
||
4. **AI 生成**(②):`ai` 模块、提示词、对照预览。
|
||
5. **更新蝦皮**(③):对已生成任务弹窗批量确认后换标题+封面、提交、回写。
|
||
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 模板,可提交;业务填写后的副本不提交
|
||
├── 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/cover/<名称>.txt
|
||
└── 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 副本属于业务数据,不提交。
|
||
|
||
## 十、架构纪律
|
||
|
||
- 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。
|