diff --git a/app/gui.py b/app/gui.py
index 09895ff..002b22e 100644
--- a/app/gui.py
+++ b/app/gui.py
@@ -10,7 +10,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
try:
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt
- from PySide6.QtGui import QPixmap
+ from PySide6.QtGui import QColor, QPixmap
from PySide6.QtWidgets import (
QAbstractItemView,
QApplication,
@@ -83,6 +83,13 @@ QTabBar::tab:hover:!selected {
background: #eaf2ff;
}
"""
+COLOR_SUCCESS = "#1a7f37"
+COLOR_DANGER = "#cf222e"
+COLOR_INFO = "#0969da"
+COLOR_PENDING = "#9a6700"
+COLOR_MUTED = "#6e7781"
+COLOR_WARNING = "#bc4c00"
+COLOR_DANGER_BG = "#ffebe9"
if QT_IMPORT_ERROR is None:
@@ -101,6 +108,35 @@ if QT_IMPORT_ERROR is None:
)
+ def _qcolor(color):
+ return QColor(color)
+
+
+ def _status_base_color(status):
+ if status == "running":
+ return COLOR_INFO
+ if status == "failed":
+ return COLOR_DANGER
+ if status in {"skipped", "cancelled"}:
+ return COLOR_MUTED
+ return None
+
+
+ def _danger_metric_text(text, active):
+ if not active:
+ return text
+ return f'{text}'
+
+
+ def _danger_outline_button_style(object_name):
+ return (
+ f"QPushButton#{object_name} {{ "
+ f"color: {COLOR_DANGER}; border: 1px solid {COLOR_DANGER}; "
+ "font-weight: 600; padding: 3px 10px; border-radius: 4px; "
+ "}"
+ )
+
+
def _database_path(db_path=None, config=None) -> str:
return db_path or appconfig.db_path(config)
@@ -298,6 +334,8 @@ if QT_IMPORT_ERROR is None:
task = self.tasks[index.row()]
if role == Qt.DisplayRole:
return self._display_value(task, index.column())
+ if role == Qt.ForegroundRole and index.column() == 3:
+ return self._stage_color(task)
if role == Qt.ToolTipRole and self.is_unmatched(task):
return "别名未匹配账号,采集时将略过"
return None
@@ -331,6 +369,16 @@ if QT_IMPORT_ERROR is None:
return self.STATUS_TEXT[task.status]
return self.STAGE_TEXT.get(task.stage, task.stage)
+ def _stage_color(self, task):
+ if self.is_unmatched(task):
+ return _qcolor(COLOR_MUTED)
+ base_color = _status_base_color(getattr(task, "status", None))
+ if base_color is not None:
+ return _qcolor(base_color)
+ if getattr(task, "stage", None) in {"collected", "generated", "applied"}:
+ return _qcolor(COLOR_SUCCESS)
+ return _qcolor(COLOR_PENDING)
+
def _display_value(self, task, column):
values = [
self._account_name(task),
@@ -397,6 +445,8 @@ if QT_IMPORT_ERROR is None:
task = self.tasks[index.row()]
if role in (Qt.DisplayRole, Qt.EditRole):
return self._display_value(task, index.column())
+ if role == Qt.ForegroundRole and index.column() == 4:
+ return self._status_color(task)
if role == Qt.ToolTipRole:
if index.column() == 3 and self._can_edit_title(task):
return "双击可微调新标题,只修改本地待更新内容"
@@ -466,6 +516,14 @@ if QT_IMPORT_ERROR is None:
return self.STATUS_TEXT[task.status]
return self.STAGE_TEXT.get(task.stage, task.stage)
+ def _status_color(self, task):
+ base_color = _status_base_color(getattr(task, "status", None))
+ if base_color is not None:
+ return _qcolor(base_color)
+ if getattr(task, "stage", None) in {"generated", "applied"}:
+ return _qcolor(COLOR_SUCCESS)
+ return _qcolor(COLOR_PENDING)
+
def _display_value(self, task, column):
values = [
self._account_name(task),
@@ -525,6 +583,11 @@ if QT_IMPORT_ERROR is None:
task = self.tasks[index.row()]
if role == Qt.DisplayRole:
return self._display_value(task, index.column())
+ if role == Qt.ForegroundRole:
+ if index.column() == 4:
+ return self._stage_color(task)
+ if index.column() == 5:
+ return self._result_color(task)
if role == Qt.ToolTipRole and task.last_error:
return task.last_error
return None
@@ -568,6 +631,23 @@ if QT_IMPORT_ERROR is None:
return "待更新"
return self.STATUS_TEXT.get(task.status, task.status)
+ def _stage_color(self, task):
+ if getattr(task, "stage", None) == "applied":
+ return _qcolor(COLOR_SUCCESS)
+ return _qcolor(COLOR_PENDING)
+
+ def _result_color(self, task):
+ base_color = _status_base_color(getattr(task, "status", None))
+ if base_color is not None:
+ return _qcolor(base_color)
+ if getattr(task, "status", None) == "pending":
+ return _qcolor(COLOR_PENDING)
+ if getattr(task, "status", None) == "success" and getattr(task, "stage", None) == "generated":
+ return _qcolor(COLOR_PENDING)
+ if getattr(task, "stage", None) == "applied" or getattr(task, "status", None) == "success":
+ return _qcolor(COLOR_SUCCESS)
+ return _qcolor(COLOR_PENDING)
+
class GenerateTab(QWidget):
"""Tab 2: prompt area plus generation task filters/list."""
@@ -1316,7 +1396,10 @@ if QT_IMPORT_ERROR is None:
self.start_update_button.setObjectName("startUpdateButton")
self.start_update_button.setMinimumWidth(118)
self.start_update_button.setStyleSheet(
- "QPushButton#startUpdateButton { font-weight: 600; padding: 6px 16px; }"
+ "QPushButton#startUpdateButton { "
+ "font-weight: 600; padding: 6px 16px; "
+ f"color: {COLOR_WARNING}; border: 1px solid {COLOR_WARNING}; "
+ "border-radius: 4px; }"
)
self.stop_update_button = QPushButton("停止")
self.reset_update_button = QPushButton("重置更新状态")
@@ -2044,9 +2127,11 @@ if QT_IMPORT_ERROR is None:
toolbar.addStretch(1)
self.summary_label = QLabel("未导入任务")
+ self.summary_label.setTextFormat(Qt.RichText)
self.match_detail_label = QLabel("")
self.show_all_button = QPushButton("全部")
self.show_unmatched_button = QPushButton("未匹配(0)")
+ self.show_unmatched_button.setObjectName("showUnmatchedButton")
summary_layout = QHBoxLayout()
summary_layout.addWidget(self.summary_label)
@@ -2620,12 +2705,17 @@ if QT_IMPORT_ERROR is None:
total = stats.get("total", len(task_rows))
valid = stats.get("valid", len(task_rows))
invalid = stats.get("invalid", 0)
+ invalid_text = _danger_metric_text(f"无效{invalid}", invalid > 0)
+ unmatched_text = _danger_metric_text(f"未匹配{unmatched}", unmatched > 0)
self.summary_label.setText(
- f"{files} 文件 · {total} 行 · 有效{valid}/无效{invalid} · 匹配{matched} · 未匹配{unmatched}"
+ f"{files} 文件 · {total} 行 · 有效{valid}/{invalid_text} · 匹配{matched} · {unmatched_text}"
)
self.match_detail_label.setText(self._match_detail(task_rows, account_rows))
self.show_unmatched_button.setText(f"未匹配({unmatched})")
self.show_unmatched_button.setEnabled(unmatched > 0)
+ self.show_unmatched_button.setStyleSheet(
+ _danger_outline_button_style("showUnmatchedButton") if unmatched > 0 else ""
+ )
if unmatched == 0 and self.model.filter_mode == "unmatched":
self.model.set_filter_mode("all")
diff --git a/docs/06-tasks.md b/docs/06-tasks.md
index e8f92d8..64dc5b2 100644
--- a/docs/06-tasks.md
+++ b/docs/06-tasks.md
@@ -94,8 +94,8 @@
| T-508 | ③ 更新shopee生产化操作区 | T-401, T-404a, T-506 | 「开始更新」作为主操作视觉强化;「重置更新状态」从底部按钮移到任务表右键菜单/高级入口,与批量更新物理分离,运行中禁用;更新安全拦截弹窗写明具体开关并提供「前往设置」深链到⑤,不改 Shopee/CDP 更新流程 | DONE |
| T-509 | ② 新标题人工微调 | T-303, T-401 | ②任务表「新标题」列可编辑;仅允许已生成、未提交线上、非运行中任务;编辑写回 `tasks.new_title`,保留 `stage=generated`,清空 `last_error`,状态回到可更新;不触碰 Shopee/CDP/Excel,③ 使用修改后的标题 | DONE |
| T-510 | ③ 检查本轮更新文案统一 | T-506, T-508 | 将用户可见「预览本轮更新」统一改为「检查本轮更新」,按钮、确认框、状态栏、完成弹窗、运行日志和文档一致;保留内部 `dry_run` 字段与执行语义,不改 Shopee/CDP 更新流程 | DONE |
-| T-511 | 语义色板 + ①②③任务状态列上色 | T-202, T-303, T-401 | 依据 `docs/ui-color-design.md`。在 `app/gui.py` 顶部定义统一语义色板常量(success `#1a7f37`/danger `#cf222e`/info `#0969da`/pending `#9a6700`/muted `#6e7781`/warning `#bc4c00`),全局引用不散落硬编码。①②③ 三个 TableModel 在 `Qt.ForegroundRole` 按内部 `stage/status` 返回 `QColor`,不要按中文显示文案硬匹配:完成态=success、失败=danger、略过/取消=muted、待处理=pending、进行中=info;不给普通行刷底色(失败行浅底 `#ffebe9` 可选、仅此一种)。保持文字+颜色双编码,不只靠颜色。跑 `test_gui.py` 无回归 | TODO |
-| T-512 | ③高风险按钮上色 + ①导入校验数字标红 | T-511, T-508, T-202b | ③「开始更新」(写线上)改用 warning `#bc4c00` 系强调(描边或填充;第一版不强制图标,避免 Windows/Qt 字体渲染不稳),与②安全流程 info 蓝主操作区分;「停止」保持中性。①导入汇总栏:未匹配数 >0 时用 danger 且保持现有按钮可点击筛出,=0 保持中性;无效行数 >0 时只标红提示,不承诺在任务表筛出,因为脏行未入库,后续若要点击查看需另做「导入错误明细」。颜色取自 T-511 色板常量 | TODO |
+| T-511 | 语义色板 + ①②③任务状态列上色 | T-202, T-303, T-401 | 依据 `docs/ui-color-design.md`。在 `app/gui.py` 顶部定义统一语义色板常量(success `#1a7f37`/danger `#cf222e`/info `#0969da`/pending `#9a6700`/muted `#6e7781`/warning `#bc4c00`),全局引用不散落硬编码。①②③ 三个 TableModel 在 `Qt.ForegroundRole` 按内部 `stage/status` 返回 `QColor`,不要按中文显示文案硬匹配:完成态=success、失败=danger、略过/取消=muted、待处理=pending、进行中=info;不给普通行刷底色(失败行浅底 `#ffebe9` 可选、仅此一种)。保持文字+颜色双编码,不只靠颜色。跑 `test_gui.py` 无回归 | DONE |
+| T-512 | ③高风险按钮上色 + ①导入校验数字标红 | T-511, T-508, T-202b | ③「开始更新」(写线上)改用 warning `#bc4c00` 系强调(描边或填充;第一版不强制图标,避免 Windows/Qt 字体渲染不稳),与②安全流程 info 蓝主操作区分;「停止」保持中性。①导入汇总栏:未匹配数 >0 时用 danger 且保持现有按钮可点击筛出,=0 保持中性;无效行数 >0 时只标红提示,不承诺在任务表筛出,因为脏行未入库,后续若要点击查看需另做「导入错误明细」。颜色取自 T-511 色板常量 | DONE |
| T-513 | 登录点 / ③Tab危险标识 / 破坏性按钮上色 | T-511, T-105, T-206 | ④登录状态 `●` 已登录=success、未登录/未检测=danger 或 muted(保留文字);③ 更新shopee Tab 若做危险标识,不依赖 QSS 硬选第 3 个 Tab,优先用 `QTabWidget.setTabIcon(2, ...)` 设置克制 warning 小圆点图标,或第一版先跳过 Tab 标识;不整条刷红。破坏性按钮「删除批次」「删除账号」文字/描边用 danger,与普通灰按钮区分(二次确认弹窗仍是主防线)。颜色取自 T-511 色板常量 | TODO |
| T-514 | ①②③ 首次空状态引导卡片 | T-205 | 依据 `docs/ux-review.md` P1。①②③ 在「无账号/未登录」时用显式空状态引导卡片替代灰按钮,例如「第一步:前往『④账号管理』配置并登录账号 →」并带跳转按钮;账号就绪后卡片消失,老用户无感。仅 UI 引导,不改采集/生成/更新流程与既有预检拦截逻辑 | TODO |
| T-515 | 批次阶段进度总览 | T-206, T-401 | 依据 `docs/ux-review.md` P1。加轻量批次进度总览:按选中批次聚合各阶段计数(导入/已采集/已生成/已更新/失败/略过),落点可选①顶部进度条或独立概览区。数据由现有 `db.list_tasks(batch_id=...)` 按 `stage/status` 聚合,不新增表;已软删除批次不计入 | TODO |
diff --git a/docs/current-state.md b/docs/current-state.md
index b94b8d1..2a3dbbd 100644
--- a/docs/current-state.md
+++ b/docs/current-state.md
@@ -6,10 +6,10 @@
## 当前快照
- 日期:2026-07-01
-- 阶段:V0 单账号 CDP 流程已验证;V1 已完成 T-000 正式代码包结构、T-001 `app/editor.py` 模块化、T-002 `app/appconfig.py` 应用配置、T-003 SQLite 持久化地基、T-004 本地数据忽略规则、T-005 AI 模型清单后端、T-006 单元测试基座、T-101 账号 user-data-dir 工具、T-102 Chrome 启动器、T-103 登录保活与检测、T-104 PySide6 主窗口骨架、T-104b PySide6 worker 基座、T-105 Tab④ 账号管理、T-106 账号快捷方式、T-201 Excel 导入入库、T-202 Tab① 任务列表与导入按钮、T-202b Tab① 导入汇总栏、T-203 采集旧标题旧封面、T-204 回写旧字段到原 Excel、T-204b 采集完成自动回写、T-205 首次未配账号/Chrome 未启动/未登录引导保护、T-205b 采集后关闭自动新建商品页 tab、T-206 Tab① 删除指定批次软删除、T-301 AI 生成接口、T-302 Tab② AI 生成布局与任务列表、T-302p 提示词管理、T-303 Tab② 开始生成/停止/进度、T-303b ②/③ 商品ID筛选、T-401 Tab③ 更新列表筛选与开始更新确认、T-402 Tab③ 确认后串行更新、T-403 Tab③ 结果回写与结束汇总、T-501 Tab⑤ AI 模型管理 UI、T-501b Tab⑤ 角色与生成参数、T-501c Tab⑤ Shopee 更新安全开关、T-502 换封面删第一张再上传、T-503 敏感信息本地明文保存提示与日志脱敏、T-504 多账号并行/dry-run/运行日志、T-207 ①采集诊断日志、② AI生成图片失败诊断日志补丁、T-404a ②/③ 选中记录重置、T-404 真实 Shopee 单条更新冒烟验收、T-506 正式使用批量更新体验、T-507 正式批量更新移除普通流程测试商品 ID 限制、T-508 ③ 更新shopee生产化操作区、T-509 ② 新标题人工微调、T-510 ③ 检查本轮更新文案统一、T-505 全流程诊断日志扩展。
+- 阶段:V0 单账号 CDP 流程已验证;V1 已完成 T-000 正式代码包结构、T-001 `app/editor.py` 模块化、T-002 `app/appconfig.py` 应用配置、T-003 SQLite 持久化地基、T-004 本地数据忽略规则、T-005 AI 模型清单后端、T-006 单元测试基座、T-101 账号 user-data-dir 工具、T-102 Chrome 启动器、T-103 登录保活与检测、T-104 PySide6 主窗口骨架、T-104b PySide6 worker 基座、T-105 Tab④ 账号管理、T-106 账号快捷方式、T-201 Excel 导入入库、T-202 Tab① 任务列表与导入按钮、T-202b Tab① 导入汇总栏、T-203 采集旧标题旧封面、T-204 回写旧字段到原 Excel、T-204b 采集完成自动回写、T-205 首次未配账号/Chrome 未启动/未登录引导保护、T-205b 采集后关闭自动新建商品页 tab、T-206 Tab① 删除指定批次软删除、T-301 AI 生成接口、T-302 Tab② AI 生成布局与任务列表、T-302p 提示词管理、T-303 Tab② 开始生成/停止/进度、T-303b ②/③ 商品ID筛选、T-401 Tab③ 更新列表筛选与开始更新确认、T-402 Tab③ 确认后串行更新、T-403 Tab③ 结果回写与结束汇总、T-501 Tab⑤ AI 模型管理 UI、T-501b Tab⑤ 角色与生成参数、T-501c Tab⑤ Shopee 更新安全开关、T-502 换封面删第一张再上传、T-503 敏感信息本地明文保存提示与日志脱敏、T-504 多账号并行/dry-run/运行日志、T-207 ①采集诊断日志、② AI生成图片失败诊断日志补丁、T-404a ②/③ 选中记录重置、T-404 真实 Shopee 单条更新冒烟验收、T-506 正式使用批量更新体验、T-507 正式批量更新移除普通流程测试商品 ID 限制、T-508 ③ 更新shopee生产化操作区、T-509 ② 新标题人工微调、T-510 ③ 检查本轮更新文案统一、T-505 全流程诊断日志扩展、T-511 语义色板与任务状态列上色、T-512 ③高风险按钮上色与①导入校验数字标红。
- 技术栈:Python 3.10+,自研 CDP(websocket-client + requests),SQLite(sqlite3)+ `config.json` + openpyxl + AI(`config/ai_models.json` 通用 HTTP,chat JSON / images_edits),GUI PySide6 5 Tab(已定)。
-- 生产代码:已建立 `app/` 包 + 根入口 `main.py`;`app/cdp.py` 为已验证 CDP 底座,已区分 `CDP.close()` 断开 WebSocket 与 `close_tab()` 关闭浏览器 target;`app/editor.py` 已封装登录状态检测、标题/封面/采集/更新按钮能力,并在采集结束后只关闭本轮自动新建的商品编辑页 tab、保留用户已有 tab,更新提交成功且设置开启时可关闭本轮自动新建商品页,确认后跳回商品列表页时关闭前等待 2 秒;`click_update()` 已补 Shopee 站点侧确认框处理,页面主「更新」后若出现 `確定您要更新商品嗎?` modal(`.eds-modal__content` / `.eds-modal__box`),只点击弹窗主按钮「更新」并避开「立即優化」,且记录确认后是否跳回商品列表页;`replace_cover()` 已实现更新封面统一先校验本地旧封面备份,再点第一张删除、可见确认框、等待图片管理器稳定和上传 input 恢复,随后先点击上传块模拟人工入口、短暂等待、重新获取 input、注入文件上传新图并等待 Shopee CDN 后拖到第一位的代码路径;已修正 2026-06-30 稳定等待回归:删除第一张前不要求上传 input 可用,只等图片列表稳定;删除后再等上传入口恢复;`重複/重复/duplicate` 上传 toast 会立即判为新封面重复错误;`app/image_paths.py` 已统一新采集/新生成图片路径为 `images///__old/new.jpg`,历史 DB 路径继续按原路径读取;`app/appconfig.py` 已实现 `config.json` 默认值/读写/更新、AI 参数、端口读取、Shopee 更新安全与执行模式默认值,`config/ai_models.json` 模型清单 CRUD/过滤/打码/测试连接与 OpenAI-compatible base URL 自动补 endpoint,以及 `mask_secret()`、`sanitize_for_log()`、`redact_secrets()` 敏感信息脱敏工具;`app/ai.py` 已实现 `gen_title()`/`gen_cover()`/`generate_batch()`,按默认文本/图片模型发起通用 HTTP 调用,支持重试、错误脱敏、图片 URL/base64 解析、resolution resize、jpg_quality 保存、先并发标题再并发封面、逐条 `set_generated`、失败 `mark_failed`、步骤级事件/错误回调与停止取消未开始项;`app/prompts.py` 已实现标题提示词读写、封面模板 CRUD 与变量替换;`app/db.py` 已实现 SQLite schema、连接 PRAGMA、批次/账号/任务与阶段写库函数、T-206 批次软删除标记与默认业务查询过滤、T-404a 本地生成结果/更新状态重置函数、T-509 `update_generated_title()` 本地新标题微调函数,以及 `run_logs/run_log_events` 运行日志函数;`app/diagnostics.py` 已实现 gitignore 本地诊断日志、滚动写入、结构化 payload 和自由文本脱敏;T-505 已把 Excel 导入/回写、③更新shopee、④Chrome 启动/登录检测、⑤AI模型测试连接接入 `run_logs/run_log_events` 与本地脱敏诊断日志;`app/excel.py` 已实现多 Excel 输入列解析、整文件列校验、脏行统计跳过、导入批次与任务入库、别名匹配统计、旧标题/旧封面路径回写原 Excel、更新结果回写原 Excel 与另存副本;`app/config.py` 已实现账号 slug 与 user-data-dir 创建;`app/accounts.py` 已实现账号 CRUD 服务、端口默认分配、启动登录、检测登录、生成快捷方式;`app/chrome.py` 已实现 Chrome 参数拼装、启动、CDP 端口探测、PowerShell `.lnk` 快捷方式生成;`app/gui.py` 已实现 PySide6 `MainWindow`、五 Tab、顶部 Tab 栏防误点样式、① 导入采集的 Excel 导入按钮/导入汇总栏/批次筛选与删除批次软删除入口/QTableView 任务列表/未匹配筛选与略过标记/采集旧标题旧封面 worker/采集前账号就绪预检与④引导/采集完成自动回写/旧数据回写重试按钮与 worker/采集运行日志视图、② AI生成左右布局/标题与封面提示词管理/批次/店铺/商品ID/状态筛选/任务列表/新标题列本地微调/变量预览/开始生成/停止/进度/双击新旧封面预览/AI生成运行日志/重置生成结果与 `GenerateWorker`、③ 更新shopee批次/店铺/商品ID/状态筛选栏/任务列表/开始更新主按钮/重置更新状态右键菜单/更新安全开关拦截与「前往设置」跳转/「检查本轮更新」按钮/开始更新确认弹窗/确认后 `ApplyWorker` 按每批最大更新条数分批执行当前筛选全部可更新记录/账号就绪和端口冲突预检/按账号并行可选/逐条 `set_applied`/运行日志/自动回写结果到 Excel/结束汇总弹窗/手动回写重试按钮、④ 账号管理表格/弹窗/按钮/快捷方式与状态栏、密码明文保存提示、⑤ 设置 AI 模型同列上下布局、居中内容区、三列详情/参数表单、API Key 明文保存提示、`AIModelTestWorker` 后台测试连接、角色/生成参数/路径/端口配置、Shopee 更新安全与多账号并行设置并持久化 `config.json`,保存成功后弹轻量提示框,`dry_run` 仅保留内部兼容字段,普通设置页已隐藏测试商品 ID 入口;③ 普通正式更新不再用 `test_item_id` 阻断非测试商品,确认弹窗不再显示测试商品 ID;`app/workers.py` 已实现 `BaseWorker`、通用 signals、取消标记和 `QThread` 启动包装。
-- 测试:`tests/` 已建立;T-006 后纯逻辑改动必须运行 `python -m unittest discover -s tests`,当前覆盖 appconfig/db/config/accounts/chrome 启动与快捷方式/editor 登录检测与商品 tab 生命周期、更新成功后关闭本轮新开 tab、更新封面备份缺失阻断/8张与9张先删第一张/删除确认/上传入口初始不可用/重复图片 toast/物流错误不阻断上传/删图后稳定等待/上传状态诊断/拖首位 mock 路径/Shopee 更新确认框、成功跳回商品列表和残留错误 toast 不覆盖成功 mock 路径/excel 导入/旧字段与更新结果回写/ai 标题与封面 HTTP 解析/`generate_batch` 正常、失败与停止/prompts 读写与渲染/gui ① 导入采集、删除批次软删除与采集诊断日志/gui ② AI生成布局与批次/店铺/商品ID/状态筛选、提示词管理、生成 worker、图片失败诊断日志、双击预览、重置生成结果与新标题本地编辑/gui ③ 更新shopee批次/店铺/商品ID/状态筛选列表、确认弹窗、开始更新主按钮、重置更新状态右键菜单、Shopee 更新安全拦截与前往设置、`ApplyWorker` 串行/检查/分批/按账号并行/端口冲突预检、运行日志、结果回写与汇总/gui ④ 账号管理、密码打码与明文保存提示/gui ⑤ AI 模型管理、API Key 打码与明文保存提示、角色/生成参数设置、Shopee 更新安全设置、检查按钮/每批最大更新条数/多账号并行设置、测试商品 ID 限制移除/worker signal 与线程包装、T-505 全流程诊断日志(import/write_back/apply/chrome_launch/login_check/ai_model_test)与本地日志脱敏;2026-07-01 已完成 5 个真实商品的 T-404 更新验收,后续 CDP/Shopee 改动仍需测试商品手动验证。
+- 生产代码:已建立 `app/` 包 + 根入口 `main.py`;`app/cdp.py` 为已验证 CDP 底座,已区分 `CDP.close()` 断开 WebSocket 与 `close_tab()` 关闭浏览器 target;`app/editor.py` 已封装登录状态检测、标题/封面/采集/更新按钮能力,并在采集结束后只关闭本轮自动新建的商品编辑页 tab、保留用户已有 tab,更新提交成功且设置开启时可关闭本轮自动新建商品页,确认后跳回商品列表页时关闭前等待 2 秒;`click_update()` 已补 Shopee 站点侧确认框处理,页面主「更新」后若出现 `確定您要更新商品嗎?` modal(`.eds-modal__content` / `.eds-modal__box`),只点击弹窗主按钮「更新」并避开「立即優化」,且记录确认后是否跳回商品列表页;`replace_cover()` 已实现更新封面统一先校验本地旧封面备份,再点第一张删除、可见确认框、等待图片管理器稳定和上传 input 恢复,随后先点击上传块模拟人工入口、短暂等待、重新获取 input、注入文件上传新图并等待 Shopee CDN 后拖到第一位的代码路径;已修正 2026-06-30 稳定等待回归:删除第一张前不要求上传 input 可用,只等图片列表稳定;删除后再等上传入口恢复;`重複/重复/duplicate` 上传 toast 会立即判为新封面重复错误;`app/image_paths.py` 已统一新采集/新生成图片路径为 `images///__old/new.jpg`,历史 DB 路径继续按原路径读取;`app/appconfig.py` 已实现 `config.json` 默认值/读写/更新、AI 参数、端口读取、Shopee 更新安全与执行模式默认值,`config/ai_models.json` 模型清单 CRUD/过滤/打码/测试连接与 OpenAI-compatible base URL 自动补 endpoint,以及 `mask_secret()`、`sanitize_for_log()`、`redact_secrets()` 敏感信息脱敏工具;`app/ai.py` 已实现 `gen_title()`/`gen_cover()`/`generate_batch()`,按默认文本/图片模型发起通用 HTTP 调用,支持重试、错误脱敏、图片 URL/base64 解析、resolution resize、jpg_quality 保存、先并发标题再并发封面、逐条 `set_generated`、失败 `mark_failed`、步骤级事件/错误回调与停止取消未开始项;`app/prompts.py` 已实现标题提示词读写、封面模板 CRUD 与变量替换;`app/db.py` 已实现 SQLite schema、连接 PRAGMA、批次/账号/任务与阶段写库函数、T-206 批次软删除标记与默认业务查询过滤、T-404a 本地生成结果/更新状态重置函数、T-509 `update_generated_title()` 本地新标题微调函数,以及 `run_logs/run_log_events` 运行日志函数;`app/diagnostics.py` 已实现 gitignore 本地诊断日志、滚动写入、结构化 payload 和自由文本脱敏;T-505 已把 Excel 导入/回写、③更新shopee、④Chrome 启动/登录检测、⑤AI模型测试连接接入 `run_logs/run_log_events` 与本地脱敏诊断日志;`app/excel.py` 已实现多 Excel 输入列解析、整文件列校验、脏行统计跳过、导入批次与任务入库、别名匹配统计、旧标题/旧封面路径回写原 Excel、更新结果回写原 Excel 与另存副本;`app/config.py` 已实现账号 slug 与 user-data-dir 创建;`app/accounts.py` 已实现账号 CRUD 服务、端口默认分配、启动登录、检测登录、生成快捷方式;`app/chrome.py` 已实现 Chrome 参数拼装、启动、CDP 端口探测、PowerShell `.lnk` 快捷方式生成;`app/gui.py` 已实现 PySide6 `MainWindow`、五 Tab、顶部 Tab 栏防误点样式、统一语义色板、①②③任务状态列前景色、③「开始更新」warning 描边/文字色和①导入校验数字标红、① 导入采集的 Excel 导入按钮/导入汇总栏/批次筛选与删除批次软删除入口/QTableView 任务列表/未匹配筛选与略过标记/采集旧标题旧封面 worker/采集前账号就绪预检与④引导/采集完成自动回写/旧数据回写重试按钮与 worker/采集运行日志视图、② AI生成左右布局/标题与封面提示词管理/批次/店铺/商品ID/状态筛选/任务列表/新标题列本地微调/变量预览/开始生成/停止/进度/双击新旧封面预览/AI生成运行日志/重置生成结果与 `GenerateWorker`、③ 更新shopee批次/店铺/商品ID/状态筛选栏/任务列表/开始更新主按钮/重置更新状态右键菜单/更新安全开关拦截与「前往设置」跳转/「检查本轮更新」按钮/开始更新确认弹窗/确认后 `ApplyWorker` 按每批最大更新条数分批执行当前筛选全部可更新记录/账号就绪和端口冲突预检/按账号并行可选/逐条 `set_applied`/运行日志/自动回写结果到 Excel/结束汇总弹窗/手动回写重试按钮、④ 账号管理表格/弹窗/按钮/快捷方式与状态栏、密码明文保存提示、⑤ 设置 AI 模型同列上下布局、居中内容区、三列详情/参数表单、API Key 明文保存提示、`AIModelTestWorker` 后台测试连接、角色/生成参数/路径/端口配置、Shopee 更新安全与多账号并行设置并持久化 `config.json`,保存成功后弹轻量提示框,`dry_run` 仅保留内部兼容字段,普通设置页已隐藏测试商品 ID 入口;③ 普通正式更新不再用 `test_item_id` 阻断非测试商品,确认弹窗不再显示测试商品 ID;`app/workers.py` 已实现 `BaseWorker`、通用 signals、取消标记和 `QThread` 启动包装。
+- 测试:`tests/` 已建立;T-006 后纯逻辑改动必须运行 `python -m unittest discover -s tests`,当前覆盖 appconfig/db/config/accounts/chrome 启动与快捷方式/editor 登录检测与商品 tab 生命周期、更新成功后关闭本轮新开 tab、更新封面备份缺失阻断/8张与9张先删第一张/删除确认/上传入口初始不可用/重复图片 toast/物流错误不阻断上传/删图后稳定等待/上传状态诊断/拖首位 mock 路径/Shopee 更新确认框、成功跳回商品列表和残留错误 toast 不覆盖成功 mock 路径/excel 导入/旧字段与更新结果回写/ai 标题与封面 HTTP 解析/`generate_batch` 正常、失败与停止/prompts 读写与渲染/gui ① 导入采集、删除批次软删除与采集诊断日志/gui ② AI生成布局与批次/店铺/商品ID/状态筛选、提示词管理、生成 worker、图片失败诊断日志、双击预览、重置生成结果与新标题本地编辑/gui ③ 更新shopee批次/店铺/商品ID/状态筛选列表、确认弹窗、开始更新主按钮、重置更新状态右键菜单、Shopee 更新安全拦截与前往设置、`ApplyWorker` 串行/检查/分批/按账号并行/端口冲突预检、运行日志、结果回写与汇总/gui ④ 账号管理、密码打码与明文保存提示/gui ⑤ AI 模型管理、API Key 打码与明文保存提示、角色/生成参数设置、Shopee 更新安全设置、检查按钮/每批最大更新条数/多账号并行设置、测试商品 ID 限制移除/worker signal 与线程包装、T-505 全流程诊断日志(import/write_back/apply/chrome_launch/login_check/ai_model_test)与本地日志脱敏、gui T-511 语义色板和任务状态列前景色、T-512 高风险按钮与导入校验数字样式;2026-07-01 已完成 5 个真实商品的 T-404 更新验收,后续 CDP/Shopee 改动仍需测试商品手动验证。
- 数据:`config.json`、`config/ai_models.json`、`cmshopee.db`、`cmshopee.db-*`、`chrome_user_data_dir/`、`images/`、`logs/` 已由 `.gitignore` 排除;密码与 API Key 本地明文保存但保存/变更时提示,UI 打码,日志/导出必须脱敏;运营填写后的 Excel 业务文件默认忽略,标准空模板 `shopee待处理任务模板.xlsx` 可提交;`app/appconfig.py` 首次读取缺失的 `config.json` 时会在本地写默认配置,`app/db.py` 调用 `init_db()` 时会在本地创建 SQLite DB。
## 既定设计要点(文档已定)
@@ -33,7 +33,7 @@
| `chrome-remote-debug-lan.md` | 已有 | WSL→Windows CDP 转发排查记录 |
| `docs/troubleshooting.md` | 已有 | 常见问题排查;已记录 `config/ai_models.json` 缺失 `category` 导致 GUI 启动报 “AI 模型 category 必须是 text 或 image” 的原因、修复和验证步骤 |
| `app/__init__.py` / `app/__main__.py` / `main.py` | 已有 | 正式包与启动入口;`python main.py` / `python -m app` 可运行占位入口 |
-| `app/gui.py` | 已有 | T-104/T-105/T-106/T-202/T-202b/T-203/T-204/T-204b/T-205/T-206/T-207/T-302/T-302p/T-303/T-303b/T-401/T-402/T-403/T-404a/T-501/T-501b/T-501c/T-503/T-504/T-506/T-507/T-508/T-509/T-510/T-505 产出:PySide6 `QMainWindow` + 五 Tab;顶部 Tab 栏防误点样式;① 导入采集导入按钮、导入汇总栏、批次筛选、删除批次软删除按钮、`QTableView` 任务列表、未匹配筛选与略过标记、采集旧标题旧封面 worker(旧封面路径按批次/店铺/任务细分)、采集前账号就绪预检与④引导、采集完成自动回写与手动重试、采集运行日志视图;② AI生成左右布局、提示词管理、批次/店铺/商品ID/状态筛选栏、任务列表、新标题列本地编辑、开始生成/停止/进度、双击新旧封面预览、AI生成运行日志、重置生成结果与 `GenerateWorker`;③ 更新shopee批次/店铺/商品ID/状态筛选栏、任务列表、开始更新主按钮、重置更新状态右键菜单、Shopee 更新安全拦截与「前往设置」跳转、「检查本轮更新」按钮、开始更新确认弹窗、`ApplyWorker` 检查/分批串行/按账号并行、账号与端口预检、运行日志、逐条 `set_applied`、自动回写结果到 Excel、结束汇总弹窗与手动回写重试;④ 账号管理表格、账号弹窗、密码本地明文保存提示、启动登录、检测登录、快捷方式;⑤ 设置 AI 模型下拉、新增/删除、详情编辑、密钥打码与本地明文保存提示、测试连接 worker、默认角色下拉、生成参数、路径端口配置、三列 Shopee 更新安全与执行模式设置,dry_run 用户入口改为③检查按钮,测试商品 ID 普通入口已隐藏且不再阻断正式更新 |
+| `app/gui.py` | 已有 | T-104/T-105/T-106/T-202/T-202b/T-203/T-204/T-204b/T-205/T-206/T-207/T-302/T-302p/T-303/T-303b/T-401/T-402/T-403/T-404a/T-501/T-501b/T-501c/T-503/T-504/T-506/T-507/T-508/T-509/T-510/T-505/T-511/T-512 产出:PySide6 `QMainWindow` + 五 Tab;顶部 Tab 栏防误点样式;统一语义色板、①②③状态列前景色、③开始更新 warning 样式和①导入校验数字标红;① 导入采集导入按钮、导入汇总栏、批次筛选、删除批次软删除按钮、`QTableView` 任务列表、未匹配筛选与略过标记、采集旧标题旧封面 worker(旧封面路径按批次/店铺/任务细分)、采集前账号就绪预检与④引导、采集完成自动回写与手动重试、采集运行日志视图;② AI生成左右布局、提示词管理、批次/店铺/商品ID/状态筛选栏、任务列表、新标题列本地编辑、开始生成/停止/进度、双击新旧封面预览、AI生成运行日志、重置生成结果与 `GenerateWorker`;③ 更新shopee批次/店铺/商品ID/状态筛选栏、任务列表、开始更新主按钮、重置更新状态右键菜单、Shopee 更新安全拦截与「前往设置」跳转、「检查本轮更新」按钮、开始更新确认弹窗、`ApplyWorker` 检查/分批串行/按账号并行、账号与端口预检、运行日志、逐条 `set_applied`、自动回写结果到 Excel、结束汇总弹窗与手动回写重试;④ 账号管理表格、账号弹窗、密码本地明文保存提示、启动登录、检测登录、快捷方式;⑤ 设置 AI 模型下拉、新增/删除、详情编辑、密钥打码与本地明文保存提示、测试连接 worker、默认角色下拉、生成参数、路径端口配置、三列 Shopee 更新安全与执行模式设置,dry_run 用户入口改为③检查按钮,测试商品 ID 普通入口已隐藏且不再阻断正式更新 |
| `app/workers.py` | 已有 | T-104b 产出:`BaseWorker` + 通用 signals + 取消标记 + `run_worker()` QThread 包装 |
| `app/accounts.py` | 已有 | T-105/T-106 产出:账号 CRUD 服务、目录创建、端口分配、启动登录、检测登录、快捷方式 |
| `app/editor.py` | 已有 | T-001/T-103/T-205b/T-207/T-501c/T-502 + T-404 补丁产出:登录状态检测、打开商品页、读/写标题、读/下载封面、采集步骤回调、更新步骤回调、采集后关闭自动新建商品页 tab、上传前等待图片管理器稳定、点击上传块后注入文件并检测 CDN 后拖封面、更新封面统一先校验旧封面备份再删线上第一张、重复图片 toast 明确失败、页面主更新按钮、Shopee 站点侧确认框主按钮、apply_task;更新成功后可按设置关闭本轮自动新建商品页 |
@@ -45,7 +45,7 @@
| `app/config.py` | 已有 | T-101 产出:别名→稳定 slug;创建并返回绝对 user-data-dir |
| `app/image_paths.py` | 已有 | 本地图片路径 helper:新采集旧封面和新生成封面统一写入 `images///__old/new.jpg`;历史 DB 已存路径继续按原路径读取 |
| `app/chrome.py` | 已有 | T-102/T-106 产出:Chrome 启动参数、`subprocess.Popen` 启动、`/json/version` 端口探测、PowerShell `.lnk` 快捷方式 |
-| `tests/` | 已有 | T-006/T-201/T-202/T-202b/T-203/T-204/T-204b/T-205/T-206/T-207/T-301/T-302/T-302p/T-303/T-303b/T-401/T-402/T-403/T-501/T-501b/T-501c/T-502/T-503/T-504/T-506/T-507/T-508/T-509/T-510/T-505 产出:stdlib unittest 基座;覆盖 appconfig/db/config/accounts/chrome/editor/excel/ai/image_paths/prompts/gui/workers |
+| `tests/` | 已有 | T-006/T-201/T-202/T-202b/T-203/T-204/T-204b/T-205/T-206/T-207/T-301/T-302/T-302p/T-303/T-303b/T-401/T-402/T-403/T-501/T-501b/T-501c/T-502/T-503/T-504/T-506/T-507/T-508/T-509/T-510/T-505/T-511/T-512 产出:stdlib unittest 基座;覆盖 appconfig/db/config/accounts/chrome/editor/excel/ai/image_paths/prompts/gui/workers |
| `app/excel.py` | 已有 | T-201/T-204/T-403 产出:多文件 Excel 输入列解析、必需列整文件拒绝、脏行逐行跳过、批次/任务入库、匹配统计;按源文件/工作表/行号回写旧标题与旧封面路径;按源文件/工作表/行号回写新标题、新封面路径、更新状态;支持原文件被占用时另存副本 |
| `shopee待处理任务模板.xlsx` | 已有,已提交 | 标准空 Excel 模板;单工作表 `待处理任务`,表头 `账号名 | 别名 | 商品id | 旧标题 | 旧封面图片路径 | 新标题 | 新封面图片路径 | 更新状态`;运营复制后填写,填写副本不提交 |
| `config.json` / `config/ai_models.json` / `cmshopee.db` / `cmshopee.db-*` / `chrome_user_data_dir/` / `images/` / `logs/` | 本地存在或按需生成,已忽略 | 含配置、密钥、业务、登录态、图片、本地诊断日志,不提交版本库;密码/API Key 保存或变更时提示,展示/日志/导出脱敏 |
@@ -63,8 +63,8 @@
任务状态以 [`06-tasks.md`](06-tasks.md) 为准,历史记录见 [`../progress.md`](../progress.md)。
-- 已完成:T-000(正式代码包结构)、T-001(`app/editor.py` 模块化)、T-002(`app/appconfig.py` + `config.json`)、T-003(SQLite 建表)、T-004(本地数据 gitignore)、T-005(AI 模型清单后端)、T-006(单元测试基座)、T-101(账号 slug/user-data-dir)、T-102(Chrome 启动器)、T-103(登录保活与检测)、T-104(PySide6 五 Tab 主窗口骨架)、T-104b(PySide6 worker 基座)、T-105(Tab④ 账号管理)、T-106(账号快捷方式)、T-201(Excel 导入:解析多文件输入列入库)、T-202(Tab① 任务列表 + 导入按钮 + 别名匹配标记)、T-202b(Tab① 导入汇总栏)、T-203(采集旧标题+旧封面)、T-204(回写旧字段到原 Excel)、T-204b(采集完成自动回写旧字段)、T-205(首次未配账号 / Chrome 未启动 / 未登录引导保护)、T-205b(采集后关闭自动新建商品页 tab)、T-206(Tab① 删除指定批次软删除)、T-301(AI 生成接口)、T-302(Tab② 左右布局与任务列表)、T-302p(提示词管理)、T-303(Tab② 开始生成 + 停止 + 进度)、T-303b(②/③ 商品ID筛选)、T-401(Tab③ 更新列表筛选 + 开始更新确认弹窗)、T-402(Tab③ 确认后串行更新)、T-403(Tab③ 结果回写与结束汇总)、T-501(Tab⑤ AI 模型管理 UI)、T-501b(Tab⑤ 角色与生成参数)、T-501c(Tab⑤ Shopee 更新安全开关)、T-502(换封面删第一张再上传)、T-503(敏感信息本地明文保存提示与日志脱敏)、T-504(多账号并行 / dry-run / 运行日志)、T-207(①采集诊断日志)、② AI生成图片失败诊断日志补丁、T-404a(②/③ 选中记录重置)、T-404(真实 Shopee 单条更新冒烟验收)、T-506(正式使用批量更新体验)、T-507(正式批量更新:移除普通流程测试商品 ID 限制)、T-508(③ 更新shopee生产化操作区)、T-509(② 新标题人工微调)、T-510(③ 检查本轮更新文案统一)、T-505(全流程诊断日志扩展)。
-- 下一个可领取任务:当前看板无 TODO;后续按产品优先级从 `docs/ux-review.md` 或新任务中领取。
+- 已完成:T-000(正式代码包结构)、T-001(`app/editor.py` 模块化)、T-002(`app/appconfig.py` + `config.json`)、T-003(SQLite 建表)、T-004(本地数据 gitignore)、T-005(AI 模型清单后端)、T-006(单元测试基座)、T-101(账号 slug/user-data-dir)、T-102(Chrome 启动器)、T-103(登录保活与检测)、T-104(PySide6 五 Tab 主窗口骨架)、T-104b(PySide6 worker 基座)、T-105(Tab④ 账号管理)、T-106(账号快捷方式)、T-201(Excel 导入:解析多文件输入列入库)、T-202(Tab① 任务列表 + 导入按钮 + 别名匹配标记)、T-202b(Tab① 导入汇总栏)、T-203(采集旧标题+旧封面)、T-204(回写旧字段到原 Excel)、T-204b(采集完成自动回写旧字段)、T-205(首次未配账号 / Chrome 未启动 / 未登录引导保护)、T-205b(采集后关闭自动新建商品页 tab)、T-206(Tab① 删除指定批次软删除)、T-301(AI 生成接口)、T-302(Tab② 左右布局与任务列表)、T-302p(提示词管理)、T-303(Tab② 开始生成 + 停止 + 进度)、T-303b(②/③ 商品ID筛选)、T-401(Tab③ 更新列表筛选 + 开始更新确认弹窗)、T-402(Tab③ 确认后串行更新)、T-403(Tab③ 结果回写与结束汇总)、T-501(Tab⑤ AI 模型管理 UI)、T-501b(Tab⑤ 角色与生成参数)、T-501c(Tab⑤ Shopee 更新安全开关)、T-502(换封面删第一张再上传)、T-503(敏感信息本地明文保存提示与日志脱敏)、T-504(多账号并行 / dry-run / 运行日志)、T-207(①采集诊断日志)、② AI生成图片失败诊断日志补丁、T-404a(②/③ 选中记录重置)、T-404(真实 Shopee 单条更新冒烟验收)、T-506(正式使用批量更新体验)、T-507(正式批量更新:移除普通流程测试商品 ID 限制)、T-508(③ 更新shopee生产化操作区)、T-509(② 新标题人工微调)、T-510(③ 检查本轮更新文案统一)、T-505(全流程诊断日志扩展)、T-511(语义色板 + ①②③任务状态列上色)、T-512(③高风险按钮上色 + ①导入校验数字标红)。
+- 下一个可领取任务:T-513(登录点 / ③Tab危险标识 / 破坏性按钮上色)。
## 当前已知限制
@@ -75,6 +75,8 @@
- T-506 已完成:③ 已将用户可见 `dry-run` 改为「检查本轮更新」按钮;真实更新对当前筛选结果按每批最大更新条数自动分批,确认弹窗显示任务数和预计批次,停止为当前商品安全结束后不再开始新任务;⑤ 设置页已改为居中内容区,左右留白已缩短为 T-506 初始实现约 40%,模型详情/角色与生成参数/路径与端口/Shopee 更新安全使用三列布局,长字段跨列;「多账号并行更新」与「最大并行账号数」已合并为同一个横向组件,最大并行账号数紧跟其后且不换行。
- T-507 已完成:普通正式更新移除 `test_item_id` 商品 ID 限制;③ 确认弹窗不再显示测试商品 ID;⑤ 普通设置页隐藏测试商品 ID,只作为历史/调试兼容字段保留;仍保留允许真实提交、允许更新封面、每批最大条数、二次确认、账号就绪预检、多账号并行上限、运行日志和 Excel 回写。
- T-508/T-509/T-510 已完成:③「开始更新」作为主操作视觉强化,「重置更新状态」移到任务表右键菜单,更新安全拦截弹窗写明具体设置并可跳到⑤;②「新标题」列允许已生成、未提交线上、非运行中任务本地微调,写回 `tasks.new_title`,清空 `last_error` 并回到可更新状态,不触碰 Shopee/CDP/Excel/封面;③ 用户可见「预览本轮更新」已统一改名为「检查本轮更新」,内部 `dry_run` 字段保留。
+- T-511 已完成:`app/gui.py` 顶部定义统一语义色板常量;①②③任务表状态相关列通过 `Qt.ForegroundRole` 按内部 `stage/status` 返回 `QColor`,完成/失败/略过/待处理/进行中分别使用 success/danger/muted/pending/info,不按中文显示文案硬匹配,不给普通行刷底色。
+- T-512 已完成:③「开始更新」使用 warning `#bc4c00` 文字和描边强调写线上风险;①导入汇总中无效行数 >0、未匹配数 >0 以 danger 标红,未匹配按钮仍保持现有点击筛出能力,无效行不新增筛表入口。
- T-301/T-303 已完成通用 HTTP AI 接口、批量生成编排和 GUI 接入 mock 单测;真实 AI 生成还需要在 `config/ai_models.json` 填入可用 url/model/api_key 后做一次成本可控的小样本实测;URL 可填完整 endpoint 或 OpenAI-compatible base URL。
- 本地 `config/ai_models.json` 若由旧版本或手工维护,可能缺少 `category`;启动报 “AI 模型 category 必须是 text 或 image” 时,按 [`troubleshooting.md`](troubleshooting.md) 只补 `category` / `enabled` 等非密钥字段,保留 API Key,且不要提交该文件。
- T-403/T-501c 已完成③更新结果回写、结束汇总与真实更新安全开关;2026-06-29 已用测试商品跑到真实编辑页并完成标题/封面替换、页面主「更新」点击,但被 Shopee 站点侧确认框拦住。当前代码已补确认框处理、确认后列表页跳转观测、上传状态诊断、删除后稳定等待和 mock 单测;2026-06-30 用户复跑发现稳定等待顺序回归:满 9 张时删除前要求上传 input 可用,导致不删除第一张图;当前已修正为删除前只等图片列表稳定、删除后再等上传入口恢复,并补 mock 回归测试;2026-06-30 又发现物流/备货页面级校验 toast 会被误判成封面上传失败,当前已修复为上传阶段只识别图片/文件/上传相关错误,物流错误留到提交阶段处理。2026-06-30 用户复跑确认浏览器已成功更新标题/图片并跳转商品列表,但 GUI 仍报 `POST_UPDATE_ERROR`;已修正提交后观测优先级:列表页跳转优先判成功,残留 error toast 不再覆盖成功跳转,错误 toast 仅在未跳转且持续存在时判失败。当前又发现商品 26887160467 手动上传 220KB 新图成功,但代码直接注入文件后上传组件长期转圈;T-404 当前补丁已让 `replace_cover()` 上传前先点击上传块模拟人工入口、短暂等待、重新获取 input 后再 `DOM.setFileInputFiles`,并补 mock 回归测试。25120403046 只有 8 张商品图时未删除第一张导致 Shopee 提示 `有1張重複的圖片`;当前已落地为更新封面统一先删当前第一张再上传,并把 `重複/重复/duplicate` toast 识别为封面上传失败。新采集/新生成图片路径已改为按批次细分,避免同一账号多批次图片混放;历史 DB 已存路径继续可用。确认后若返回我的商品列表页且设置了成功后关闭本次新开编辑页,关闭前等待 2 秒。2026-07-01 用户已用包含 5 个商品 ID 的 Excel 完成导入、AI 生成标题/图片、Tab③ 更新到 Shopee 的真实链路验收,T-404 判定完成。
diff --git a/progress.md b/progress.md
index c46fd69..8601c79 100644
--- a/progress.md
+++ b/progress.md
@@ -932,3 +932,17 @@
- CDP:`app/editor.py` 只给 `apply_task()` 增加可选 `on_step` 回调,用于上报 `open_product/change_title/replace_cover/click_update`,不改选择器、上传方式、确认框点击或提交逻辑。
- 安全:DB 业务日志和本地 `logs/cmshopee.log` 均通过现有脱敏工具处理,不记录 Cookie、密码、API Key、token;异常路径本地日志保留脱敏 traceback。
- 测试:新增/更新 `tests/test_gui.py` 覆盖 import/write_back/apply/chrome_launch/login_check/ai_model_test 运行日志和本地日志脱敏;`python -m py_compile app\gui.py app\editor.py tests\test_gui.py` 通过;`python -m unittest discover -s tests -p test_gui.py` 通过(63 tests);`python -m compileall app main.py` 通过;`python -m unittest discover -s tests` 通过(151 tests);`git diff --check` 无空白错误,仅 LF/CRLF 提示。
+## 【2026-07-01】T-511 完成 · 语义色板与任务状态列上色
+
+- 范围:按 `docs/ui-color-design.md` 落地第一步状态列上色,只改 GUI 展示层,不改 CDP、Shopee 提交流程、DB schema 或业务状态流转。
+- 代码:`app/gui.py` 顶部新增统一语义色板常量(success/danger/info/pending/muted/warning/danger-bg),`TaskTableModel`、`GenerateTaskTableModel`、`ApplyTaskTableModel` 在状态相关列响应 `Qt.ForegroundRole`,按内部 `stage/status` 返回 `QColor`;完成态用 success、失败用 danger、略过/取消用 muted、待处理用 pending、进行中用 info,不按中文显示文案硬匹配,不给普通行刷底色。
+- 测试:`tests/test_gui.py` 新增状态列前景色断言,覆盖①待采集/未匹配、②待生成/已生成/失败、③待更新/失败/已更新。
+- 验证:`python -m py_compile app\gui.py tests\test_gui.py` 通过;`python -m unittest discover -s tests -p "test_gui.py"` 通过(63 tests);`python -m compileall app main.py` 通过;`python -m unittest discover -s tests` 通过(151 tests)。测试输出仍有 PySide6 字体目录提示,不影响结果。
+- 下一步:T-512「③高风险按钮上色 + ①导入校验数字标红」。
+## 【2026-07-01】T-512 完成 · 高风险按钮与导入校验数字标红
+
+- 范围:按 `docs/ui-color-design.md` 落地第二步颜色优化,只改 GUI 样式层,不改 Tab 名、不改 CDP、Shopee 提交流程、DB schema 或业务状态流转。
+- 代码:③「开始更新」按钮使用 warning `#bc4c00` 文字和描边,保留「停止」中性;①导入汇总使用 rich text 将无效行数 >0、未匹配数 >0 标为 danger `#cf222e`;未匹配按钮 >0 时同样使用 danger 描边/文字并保持可点击筛出,=0 时清空样式并禁用;无效行仅标红提示,不新增筛表入口。
+- 测试:`tests/test_gui.py` 新增开始更新按钮 warning 样式、未匹配按钮 danger 样式、无效行 rich text danger 标记断言。
+- 验证:`python -m py_compile app\gui.py tests\test_gui.py` 通过;`python -m unittest discover -s tests -p "test_gui.py"` 通过(63 tests);`python -m compileall app main.py` 通过;`python -m unittest discover -s tests` 通过(151 tests)。测试输出仍有 PySide6 字体目录提示,不影响结果。
+- 下一步:T-513「登录点 / ③Tab危险标识 / 破坏性按钮上色」。
diff --git a/tests/test_gui.py b/tests/test_gui.py
index 43f6c68..e017702 100644
--- a/tests/test_gui.py
+++ b/tests/test_gui.py
@@ -77,6 +77,11 @@ class GuiTests(TempDirMixin, unittest.TestCase):
}
return cfg
+ def assert_foreground(self, model, row, column, color):
+ value = model.data(model.index(row, column), gui.Qt.ForegroundRole)
+ self.assertIsNotNone(value)
+ self.assertEqual(color, value.name())
+
def test_main_window_has_five_tabs_in_workflow_order(self):
with self.make_temp_dir() as temp_dir:
window = MainWindow(config=self.make_config(temp_dir))
@@ -983,6 +988,8 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertTrue(tab.reset_update_button.isHidden())
self.assertEqual(gui.Qt.CustomContextMenu, tab.task_table.contextMenuPolicy())
self.assertEqual("startUpdateButton", tab.start_update_button.objectName())
+ self.assertIn(gui.COLOR_WARNING, tab.start_update_button.styleSheet())
+ self.assertIn("border", tab.start_update_button.styleSheet())
self.assertFalse(tab.stop_update_button.isEnabled())
self.assertEqual(2, tab.model.rowCount())
self.assertEqual("主店", tab.model.index(0, 0).data())
@@ -990,12 +997,15 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual("新标题10", tab.model.index(0, 2).data())
self.assertEqual("51100639510_new.jpg", tab.model.index(0, 3).data())
self.assertEqual("待更新", tab.model.index(0, 4).data())
+ self.assert_foreground(tab.model, 0, 4, gui.COLOR_PENDING)
self.assertEqual("待更新", tab.model.index(0, 5).data())
+ self.assert_foreground(tab.model, 0, 5, gui.COLOR_PENDING)
self.assertEqual("任务 2/4 条", tab.summary_label.text())
tab.status_filter.setCurrentIndex(tab.status_filter.findData("failed"))
self.assertEqual(1, tab.model.rowCount())
self.assertEqual("失败", tab.model.index(0, 5).data())
+ self.assert_foreground(tab.model, 0, 5, gui.COLOR_DANGER)
self.assertEqual(
"更新失败",
tab.model.data(tab.model.index(0, 0), gui.Qt.ToolTipRole),
@@ -1011,7 +1021,9 @@ class GuiTests(TempDirMixin, unittest.TestCase):
tab.status_filter.setCurrentIndex(tab.status_filter.findData("applied"))
self.assertEqual(1, tab.model.rowCount())
self.assertEqual("已更新", tab.model.index(0, 4).data())
+ self.assert_foreground(tab.model, 0, 4, gui.COLOR_SUCCESS)
self.assertEqual("成功", tab.model.index(0, 5).data())
+ self.assert_foreground(tab.model, 0, 5, gui.COLOR_SUCCESS)
tab.batch_filter.setCurrentIndex(tab.batch_filter.findData(None))
tab.status_filter.setCurrentIndex(tab.status_filter.findData("all"))
@@ -1931,7 +1943,9 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual("旧标题A", tab.model.index(0, 2).data())
self.assertEqual("", tab.model.index(0, 3).data())
self.assertEqual("待生成", tab.model.index(0, 4).data())
+ self.assert_foreground(tab.model, 0, 4, gui.COLOR_PENDING)
self.assertEqual("已生成", tab.model.index(2, 4).data())
+ self.assert_foreground(tab.model, 2, 4, gui.COLOR_SUCCESS)
tab.status_filter.setCurrentIndex(tab.status_filter.findData("to_generate"))
self.assertEqual(2, tab.model.rowCount())
@@ -1946,6 +1960,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
tab.batch_filter.setCurrentIndex(tab.batch_filter.findData(batch_b))
self.assertEqual(1, tab.model.rowCount())
self.assertEqual("失败", tab.model.index(0, 4).data())
+ self.assert_foreground(tab.model, 0, 4, gui.COLOR_DANGER)
self.assertEqual(
"生成失败",
tab.model.data(tab.model.index(0, 0), gui.Qt.ToolTipRole),
@@ -2141,13 +2156,18 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual("alias-a", tab.model.index(0, 1).data())
self.assertEqual("51100639510", tab.model.index(0, 2).data())
self.assertEqual("待采集", tab.model.index(0, 3).data())
+ self.assert_foreground(tab.model, 0, 3, gui.COLOR_PENDING)
self.assertEqual("Excel副店", tab.model.index(1, 0).data())
self.assertEqual("missing", tab.model.index(1, 1).data())
self.assertEqual("略过", tab.model.index(1, 3).data())
+ self.assert_foreground(tab.model, 1, 3, gui.COLOR_MUTED)
self.assertIn("2 行", tab.summary_label.text())
self.assertIn("有效2/无效0", tab.summary_label.text())
self.assertIn("匹配1", tab.summary_label.text())
self.assertIn("未匹配1", tab.summary_label.text())
+ self.assertIn(gui.COLOR_DANGER, tab.summary_label.text())
+ self.assertIn(gui.COLOR_DANGER, tab.show_unmatched_button.styleSheet())
+ self.assertIn("border", tab.show_unmatched_button.styleSheet())
self.assertIn("主店1", tab.match_detail_label.text())
self.assertIn("未匹配", tab.empty_label.text())
@@ -2326,8 +2346,11 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual("alias-a", tab.model.index(0, 1).data())
self.assertIn("1 文件", tab.summary_label.text())
self.assertIn("2 行", tab.summary_label.text())
- self.assertIn("有效1/无效1", tab.summary_label.text())
+ self.assertIn("有效1/", tab.summary_label.text())
+ self.assertIn("无效1", tab.summary_label.text())
+ self.assertIn(gui.COLOR_DANGER, tab.summary_label.text())
self.assertIn("匹配1", tab.summary_label.text())
+ self.assertEqual("", tab.show_unmatched_button.styleSheet())
self.assertIn("入库1", statuses[-1])
self.assert_removed(temp_dir)