feat: 完成T-501c Shopee更新安全开关

新增 shopee_update 配置段,默认关闭真实提交和封面更新,限制测试商品ID、单次最大更新条数,并支持成功后关闭本轮自动新开编辑页。

Tab⑤ 设置页新增 Shopee 更新安全表单;Tab③ 开始更新前先读取安全配置并阻断未授权真实提交、超量、非测试商品和未允许的封面更新。

ApplyWorker 将 close_success_tab 传入 editor.apply_task;editor 仅在提交成功且页面为本轮自动新开时关闭 tab,失败和复用页面保留。

同步架构、API、路由、任务看板、当前状态与 progress 文档;补充 GUI 与 editor 单测覆盖安全设置保存、共享配置读取、安全拦截和 tab 关闭策略。
This commit is contained in:
chengma
2026-06-29 09:18:03 +08:00
parent 91683abe7d
commit b05a856796
11 changed files with 453 additions and 42 deletions
+16 -10
View File
@@ -32,7 +32,7 @@ ai_config(config=None) -> dict # default_text_model/default_image
response_timeout(config=None) -> int # = resolution_timeouts[resolution](返回超时,随分辨率)
```
`config.json` 不保存 AI Key;写入 `api_key` / `*_key` / `token` / `*_token` / `password` / `*_password` 等敏感字段时抛 `ConfigError`。AI Key 留给 `config/ai_models.json`。
`default_config()` / `load_config()` 包含 `shopee_update` 安全配置段:测试商品 ID、是否允许真实提交、是否允许封面更新、单次最大更新条数、成功后是否关闭本轮新开编辑页。`config.json` 不保存 AI Key;写入 `api_key` / `*_key` / `token` / `*_token` / `password` / `*_password` 等敏感字段时抛 `ConfigError`。AI Key 留给 `config/ai_models.json`。
AI 模型清单(`config/ai_models.json`,含本地明文密钥,已建;UI 由 ⑤ 设置复用):
@@ -217,8 +217,10 @@ collect(account, task) -> dict # -> {old_title, old_cover_path}
change_title(cdp, new_title) -> dict # {ok, value, modelvalue},要求三者相等
replace_cover(cdp, image_win_path) -> dict # 上传→等CDN→拖第一位;满9张暂返回 FULL_IMAGE_SLOTS,删除流程留给 T-502
click_update(cdp) -> dict # {clicked, reason};禁用则记失败
apply_task(account, task) -> dict # 对已生成任务:换标题+换封面+点「更新」提交(调用前必须已做批量确认)
# -> {committed, error}
apply_task(account, task, close_success_tab=False) -> dict
# 对已生成任务:换标题+换封面+点「更新」提交(调用前必须已通过安全开关和批量确认)
# close_success_tab=True 时,仅成功提交且商品页为本轮自动新开时关闭该 tab
# -> {committed, error}
```
`login_status()` 不自动登录;无 Shopee tab 时打开卖家中心根地址 `https://<region_host>/`(默认 `https://seller.shopee.tw/`)用于检测/人工登录。判断规则:最终 URL 是登录页 → `LOGIN_PAGE`;缺少 `SPC_ST`/`SPC_U` → `NO_SESSION_COOKIE`;有会话 Cookie → 已登录。
@@ -228,7 +230,7 @@ apply_task(account, task) -> dict # 对已生成任务:换标题+
- `CDP.close()` 只断开当前 websocket 控制连接,不关闭 Chrome 页面。
- `open_product()` 若复用已存在商品 tab,则标记为用户已有页面;若调用 `create_tab()` 新建,则记录 target id。
- `collect()` 结束时只关闭本轮自动新建的商品编辑页 tab;用户原本打开的商品 tab 不关闭。
- ③ 更新流程暂不自动关闭商品页:成功后是否关闭留待后续体验任务,失败时保留现场便于排查。
- ③ 更新流程默认不关闭商品页;若 `close_success_tab=True`,只在提交成功且商品页为本轮自动新开时关闭,失败任务和用户原本打开的 tab 保留现场。
## ai 模块(`app/ai.py`,已建,外部 AI,通用 HTTP)
@@ -296,11 +298,11 @@ main() -> int # 创建 QApplication + MainWindow
class MainWindow(QMainWindow) # QTabWidget: ①②③④⑤;支持注入 db_path/config/config_path/ai_models_path 便于测试
class CollectTab(QWidget) # ① 导入采集:导入 Excel + 汇总栏 + QTableView 任务列表 + 未匹配略过标记
class GenerateTab(QWidget) # ② AI生成:提示词管理 + 筛选任务 + 开始/停止生成 + 新旧封面预览
class ApplyTab(QWidget) # ③ 更新shopee:筛选已生成任务 + 确认后串行执行更新
class SettingsTab(QWidget) # ⑤ 设置:AI 模型 master-detail 管理
class ApplyTab(QWidget) # ③ 更新shopee:筛选已生成任务 + 安全开关拦截 + 确认后串行执行更新
class SettingsTab(QWidget) # ⑤ 设置:AI 模型管理 + 角色/生成参数/路径端口 + Shopee 更新安全
class CollectWorker(BaseWorker) # ① 后台采集:账号就绪预检 -> editor.collect -> db.set_collected/mark_skipped/mark_failed
class GenerateWorker(BaseWorker) # ② 后台生成:ai.generate_batch -> db.set_generated/mark_failed + 进度
class ApplyWorker(BaseWorker) # ③ 后台更新:账号就绪预检 -> editor.apply_task -> db.set_applied/mark_skipped
class ApplyWorker(BaseWorker) # ③ 后台更新:账号就绪预检 -> editor.apply_task(close_success_tab=...) -> db.set_applied/mark_skipped
class WriteBackWorker(BaseWorker) # ①/③ 后台回写:旧字段或更新结果写回原 Excel
class AIModelTestWorker(BaseWorker) # ⑤ 后台测试 AI 模型连接:appconfig.test_ai_model
class TaskTableModel(QAbstractTableModel) # 任务表格模型:账号/别名/商品ID/阶段;未匹配别名显示“略过”
@@ -333,6 +335,8 @@ TAB_STYLE: str # 顶层 Tab 栏防误点样式:
- 角色与生成参数读写 `config.json`:标题大模型(仅 text)、图片大模型(仅 image)、标题/图片并发、失败重试、分辨率、jpg 质量。
- 分辨率下拉固定 `512/1k/2k/4k`;返回超时标签只读展示 `resolution_timeouts[resolution]`。
- 路径与端口读写 `config.json`:Chrome 路径、账号数据根目录、图片目录、DB 路径、默认调试端口、调试端口范围、CDP 就绪超时。保存时校验端口范围和默认端口。
- Shopee 更新安全读写 `config.json` 的 `shopee_update` 段:测试商品 ID、允许真实提交、允许更新封面、单次最大更新条数、成功后关闭本次新开编辑页。
- 真实提交与封面更新默认关闭;用户在 ⑤ 保存开启后,③ 仍需要通过安全开关检查并弹窗确认,才会创建更新 worker。
① 导入采集当前要点(T-202/T-202b):
@@ -368,10 +372,12 @@ TAB_STYLE: str # 顶层 Tab 栏防误点样式:
- 状态筛选支持:已生成(默认,`stage=generated` 且 `status=success/pending`)、失败、已更新、略过、全部状态。
- 任务列表使用 `QTableView + ApplyTaskTableModel`,列为:店铺、商品ID、新标题、新封面、阶段、结果。
- 「开始更新」只读取当前筛选结果;无任务时只提示,不弹确认、不改库。
- 点击「开始更新」会弹窗展示批次/店铺/状态/任务数和提交线上风险;用户点否/取消时不执行、不改库;用户点是后才创建 `ApplyWorker` 串行执行更新。
- 点击「开始更新」先读取 `shopee_update`:未允许真实提交、超过单次最大条数、包含非测试商品 ID、或包含新封面但未允许封面更新时,弹警告并阻断;通过后才弹窗展示批次/店铺/状态/任务数、提交线上风险和当前安全设置。
- 用户点否/取消时不执行、不改库;用户点是后才创建 `ApplyWorker` 串行执行更新。
- `ApplyWorker` 只处理当前筛选结果里 `stage=generated` 且已有新标题或新封面、状态为 `success/pending/failed` 的任务;已更新和略过记录仅查看,不会再次提交。
- 更新前先做账号就绪预检:无账号、当前筛选结果匹配账号 Chrome 未启动或未登录时,返回 `blocked=True`,GUI 弹窗汇总并跳转/引导去④账号管理;预检不通过时不调用 `editor.apply_task()`、不写失败状态。
- 预检通过后逐条 `db.mark_running(..., "apply")` → `editor.apply_task(account, task)` → `db.set_applied()`;成功推进 `stage=applied/status=success/committed=1`,失败保持原 stage、`status=failed/committed=0/last_error`,单条失败继续下一条。
- 预检通过后逐条 `db.mark_running(..., "apply")` → `editor.apply_task(account, task, close_success_tab=设置值)` → `db.set_applied()`;成功推进 `stage=applied/status=success/committed=1`,失败保持原 stage、`status=failed/committed=0/last_error`,单条失败继续下一条。
- 若 `close_success_tab=true`,`editor.apply_task()` 只关闭本轮自动新开且成功提交的商品页;失败和复用的用户已有 tab 不关闭。
- 别名未匹配账号的任务逐条 `db.mark_skipped()`,原因 `别名未匹配账号`;「停止」调用 worker 协作式 `cancel()`,已开始单条跑到安全边界后结束。
- ③ 没有常驻提交开关;确认弹窗是提交线上前的边界。
- 更新完成后自动调用 `WriteBackWorker(mode="results")` → `excel.write_back_results()`,把新标题、新封面路径、更新状态写回原 Excel;原文件被占用时提示关闭后点击「回写结果到 Excel」手动重试,SQLite 更新结果不回滚。
@@ -409,7 +415,7 @@ run_worker(worker: BaseWorker, thread_name=None, start=True) -> QThread
- 采集、AI 生成、更新、Excel 回写都通过 worker 执行,用 signal 回传进度。
- 每个 worker/线程按需创建自己的 SQLite connection,不跨线程共享连接。
- ③ 的批量确认弹窗在 GUI 主线程完成;用户确认后才创建 `ApplyWorker`。
- `ApplyWorker` 串行调用 `editor.apply_task()`,逐条 `set_applied()`,失败继续;账号未就绪时整体阻断并引导④,不进入逐条提交。
- `ApplyWorker` 串行调用 `editor.apply_task(..., close_success_tab=...)`,逐条 `set_applied()`,失败继续;账号未就绪时整体阻断并引导④,不进入逐条提交。
- `WriteBackWorker` 默认 `mode="old"` 回写旧字段;③ 使用 `mode="results"` 回写新标题/新封面/更新状态,支持单批次或多批次列表。
- `execute()` 未捕获异常会发 `failed(-1, error)` 与 `finished({"ok": False, "error": ...})`;普通单行失败由业务 worker 自己发 `failed(task_id, error)` 后继续处理。