--- id: T-628 title: 采集当前记录阶段与单条耗时指示器 phase: 2 deps: [T-620] status: TODO created: 2026-07-14 --- ## 问题 / 背景 ①「导入采集」处理商品详情页时,打开页面、等待加载和下载封面都可能持续较长时间。当前页面只有静态的「批次进度」汇总、底部状态栏和运行日志,用户在一条记录长时间未结束时难以判断采集是否仍在执行。 已核实的现状: - `collectBatchProgressOverview` 是根据 SQLite 任务快照生成的汇总 label,`refresh_tasks()` 时会整体重写文本,本身没有运行计时语义。 - `CollectWorker.progress` 主要在一条记录结束后发出,不能反映当前记录正在执行哪个步骤。 - `row_updated(status=running)` 在单条记录登录状态检查之后才发出;若以它作为计时起点,会漏掉前置检查和本条登录检查耗时。 - `editor.collect()` 已有 `open_product / wait_ready / read_title / read_cover / download_cover` 等步骤回调,可作为结构化阶段来源。 顾客希望在「批次进度」同行右侧增加动态计时,并在每条记录开始时重置,让用户能看到当前采集任务仍处于运行状态。 计时数字持续变化只能证明 GUI 计时器仍在运行,不能证明 CDP 一定在继续推进。因此本任务把需求优化为「**当前阶段 + 当前记录耗时**」指示器:阶段显示最后一次已知进展,秒数显示该记录已经等待多久,不向用户承诺任务一定健康。 ## 方案 ### 1. 批次进度同行增加独立运行指示器 - 在现有「批次进度」右侧增加独立 label,不把计时字符串拼入 `batch_progress_label`,避免 `refresh_tasks()` 重写批次汇总时覆盖计时内容。 - 两个 label 放入同一水平布局:批次汇总占可伸缩区域,运行指示器靠右并按最长合理文案预留稳定宽度,秒数变化不得造成布局抖动。 - 运行中推荐文案:`正在采集 3/20 · 等待商品页加载 · 本条 00:18`。 - 窗口空间不足时,允许批次汇总自然换行;运行状态、序号和时间必须保持可见,完整商品 ID / 账号可放在中文 tooltip 中,不在紧凑 label 内强行堆叠。 - 不使用纯装饰性转圈或闪烁动画;每秒变化的计时和明确的「正在采集」文字即为运行反馈。 ### 2. 使用结构化 Worker 活动信号 - 为 `CollectWorker` 增加专用结构化活动信号,例如 `activity: Signal(dict)`;不得通过解析用户日志文本推断当前任务或步骤。 - 活动 payload 至少支持: - `preflight_started`:开始检查账号和 Chrome; - `task_started`:一条 eligible 记录开始处理; - `task_step`:当前记录进入新步骤; - `task_finished`:当前记录成功、失败或略过。 - payload 使用稳定字段,如 `state / index / total / task_id / item_id / alias / step / result`,不得把账号密码、Cookie、token 或未脱敏异常放入信号。 - `task_started` 应在进入每条记录处理循环后尽早发出,覆盖本条账号匹配、登录检查和后续采集耗时;不得等到 `db.mark_running()` 后才开始计时。 - `editor.collect()` 的步骤回调继续用于日志,同时把步骤作为结构化 `task_step` 发给 GUI。阶段映射集中维护并全部显示中文,至少覆盖:检查账号、打开商品页、等待商品页加载、读取标题、读取封面、下载封面、保存采集结果。 - 结构化活动信号只增加可观察性,不改变原有 `progress / row_updated / log / failed / finished / cancelled` 的业务语义。 - 本轮正常完成、停止和前置阻断继续以现有 `finished / cancelled` 信号及 summary 中的 `blocked` 字段为唯一终态来源,不在 `activity` 中复制一套终态协议。 ### 3. 计时口径与生命周期 - GUI 使用父对象为 `CollectTab` 的 `QTimer`,每 1000ms 刷新一次;真实耗时通过 `time.monotonic()` 计算,不通过简单 `seconds += 1` 累加。 - 点击「采集旧标题/旧封面」并成功创建 Worker 后,记录批次开始时间,显示 `正在检查账号 · 00:00`。 - 收到 `task_started` 时重置当前记录起点并开始显示 `本条 00:00`;收到 `task_step` 只更新阶段文字,不重置本条计时。 - 下一条记录开始时重新从 `00:00` 计时;成功、失败或略过可短暂冻结本条耗时,随后由下一条 `task_started` 覆盖。 - 用户点击「停止」后,指示器立即改为 `正在停止 · 本条 00:18`,在当前不可中断步骤退出前继续显示等待时间;收到 `cancelled` 后再冻结为终态,不能提前伪装成已经停止。 - 正常完成、前置检查阻断、用户停止、Worker 异常及线程释放路径都必须停止 QTimer、清理当前任务引用并冻结最终文字,不能在采集结束后继续走秒。 - 本轮终态文案应区分: - 正常完成:`采集完成 · 总用时 08:32`; - 用户停止:`采集已停止 · 总用时 03:10`; - 前置阻断:`采集未开始 · 检查未通过`; - 异常结束:`采集已结束 · 请查看运行日志`。 - 计时只用于本次 GUI 显示,不写入 SQLite 或 Excel;现有逐条诊断日志中的 `elapsed_ms` 保持不变。 ### 4. 状态语义与视觉边界 - 运行中使用现有信息色/进行中色,并保留「正在采集」文字,不能只靠颜色表达状态。 - 完成、停止、阻断和异常沿用项目既有状态语义色;不增加“卡死”自动判定。页面加载和图片下载可能合法地持续较久,仅凭固定秒数变色会产生误报。 - 用户看到持续增加的本条耗时,应理解为“该记录尚未结束”;阶段长时间不变化时可据此决定查看日志或停止,但 UI 不显示“运行正常”等未经验证的结论。 ## 验收要点 - 有批次任务但未运行时,批次汇总显示正常,右侧运行指示器不误报正在采集。 - 点击采集后,在首条记录开始前显示中文前置检查状态和持续计时;GUI 保持可响应。 - 第一条记录开始时显示 `1/N` 并从 `00:00` 计时;步骤变化时阶段文字变化、本条计时不重置。 - 第二条记录开始时显示 `2/N`,本条时间重新从 `00:00` 开始。 - 成功、失败和略过记录都能进入正确的下一条计时;不会因为某条失败而停止整轮指示器。 - 正常完成、前置阻断、用户停止、Worker 异常和线程收尾后计时全部停止,等待数秒后界面秒数不再变化。 - 批次进度汇总刷新不会覆盖运行指示器;运行指示器更新也不会改变批次阶段统计。 - 运行状态 label 宽度稳定,在最小支持窗口及 Windows 100% / 125% / 150% 显示缩放下无重叠和明显布局跳动。 - 单测使用可控 `time.monotonic()` 或注入时间验证,不通过真实 `sleep()` 拉长测试。 - 自动验证:`py -3.10 -m unittest discover -s tests`、`python -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`git diff --check` 全绿。 ## 边界(不改什么) - 不修改 CDP 选择器、商品页打开/关闭策略、Chrome 前后台切换、登录判定或采集重试策略。 - 不修改任务 `stage/status`、SQLite schema、Excel schema、回写流程或批次统计口径。 - 不把计时器放进 Worker 线程,不让 GUI 每秒查询 SQLite,也不把每秒 tick 写入运行日志。 - 不改变②AI生成、③更新蝦皮及⑥商品套图的计时显示。