Files
cmbuyer/docs/tasks/T-304.md
T

137 lines
14 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.
---
id: T-304
title: 定时轮询、会话边界与采购工具主界面
phase: 3
deps: [T-303, T-006]
status: DOING
created: 2026-08-04
vikunja_task_id: 37
context_ref: 526af1e
work_branch: task/t-304-polling-session-ui
needs_device: false
needs_human_review: true
write_paths:
- docs/tasks/T-304.md
- client/src/cmbuyer_client/app.py
- client/src/cmbuyer_client/polling/**
- client/src/cmbuyer_client/ui/**
- client/src/cmbuyer_client/localstate/models.py
- client/src/cmbuyer_client/localstate/store.py
- client/tests/test_app.py
- client/tests/polling/**
- client/tests/ui/**
- client/tests/localstate/test_store.py
---
<!-- BEGIN VIKUNJA EXPORT id=37 synced=2026-08-05T01:23:52Z sha256=7d7650322518979f6122417383d74ee2c399f3c93eacd6935c178c93dbb59e11 -->
## 问题 / 背景
T-006 已确认采购工具双 Tab 原型,T-303 将提供唯一的 HTTP、本地恢复、DPAPI 与单实例底座。T-304 把原型转换为可测试的原生 PySide6 主界面和定时轮询协调器,但在真实单趟执行器接入前不得领取真实任务,避免形成无人消费的 active claim。
## 关联需求与交互
- 功能:F-005、F-008、F-012、F-013。
- 用户故事:US-003、US-004、US-007。
- 依赖:T-303、T-006;T-302/T-301/T-203/T-204 由 T-303 传递满足。
- 实现已确认的“采购工具”固定双 Tab:默认“采购执行”,另一个为“配置”。
- 不接入真机选择、事件、围栏、提交或付款;T-305 才注入受控单趟执行器。
## 方案
1. 使用 `QMainWindow + QTabWidget` 建立不可关闭的固定 Tab,使用稳定 page id 而非显示索引;Tab 切换保留内部状态、选择、滚动和焦点。
2. 采购执行页顶部显示连接、轮询、当前任务与连续失败状态,开始/停止复用同一 `QAction`。独立运行且未注入 execution consumer 时开始按钮禁用,并显示“单趟执行能力尚未接入,不能领取真实任务”。
3. 主体使用 `QSplitter`。左侧 `QStackedWidget` 在“当前任务”和“采购记录详情”间切换;当前任务左侧文字、右侧图片约 2:1,下方为滚动日志。右侧使用 `QTableView + QAbstractTableModel` 显示按时间倒序的标题和状态,不放假记录。
4. 记录表单击只选择;双击非控件区域、Enter、显式“查看所选记录”和上下文命令复用同一 QAction。详情态选择另一行立即更新;Esc 仅从详情返回当前任务,不停止轮询、不关闭程序、不改变服务端状态,并恢复记录选择、滚动和焦点。
5. 配置页使用 `QScrollArea + QFormLayout` 显式保存。service URL 为只读精确 `http://127.0.0.1:8080`;token 使用密码框且永不回填,只显示“已保存/未保存”布尔状态。首次保存且 `has_stored_device_token=false` 时 token 必填;已有凭据时,规范化后的空输入表示保留现有 DPAPI 密文,非空输入才通过 T-303 原子 profile API 替换。成功后清空输入框;失败保留全部输入并聚焦首个错误。MVP 不提供清除/删除凭据命令,空值绝不能覆盖或删除既有凭据。
6. 配置承载 device id、ADB 路径、serial/transport、轮询间隔默认 15 秒且范围 5–300、连续失败阈值默认 3 且范围 1–10、HTTP 超时默认 10 秒且范围 1–120、真机步骤超时默认 45 秒且范围 5–300。只执行必填、格式、范围以及本地路径形态/存在性校验,不执行路径所指程序;pending/active claim 时冻结 service/device 身份。
7. 配置页不提供“检查连接/测试连接”按钮、QAction、worker 或隐藏探测。保存和本地校验不得发送 HTTP health、`claim-next`、`renew`、`evidence`,也不得调用 ADB、uiautomator2 或拼多多。稳定内联提示为“服务身份将在首次真实领取时验证;设备与 App 状态由后续已取证执行能力验证”。本地校验通过不等于服务、设备或 App 已就绪。
8. 轮询协调器状态固定为 STOPPED、STARTING、BLOCKED、RECOVERING、WAITING、CLAIMING、ACTIVE、RECOVERY_REQUIRED。启动必须先取得 T-303 单实例 guard 并加载 recovery snapshot;重启不自动开始,active claim 只显示待安全恢复。
9. 停止只设置 `accept_new_claims=false`。WAITING 取消下一计时器;CLAIMING 等待有界结果并先落库;返回 claim 后进入 RECOVERY_REQUIRED。不得中断已发请求、清 pending/active、release/abandon、生成新 key/session 或调用真机返回动作。
10. `QTimer` 只调度下一轮;阻塞 I/O 放长期 worker QObject/QThread,worker 不接触 Widget。claim 业务结果必须先交 T-303 持久化,不能因 UI generation 过期而丢弃;迟到的纯视图结果按稳定 request id 丢弃。
11. 连续失败只统计 T-303 标记为可用同幂等键安全重放的网络、超时、5xx 或截断;同 pending request 按轮询间隔重放,达到阈值立即停止。401、协议错误、409、DPAPI/SQLite 错误立即转持久 Banner,不进入普通重试。
## 验收要点
- 纯状态机覆盖开始、EMPTY、等待、停止、请求中停止、claim 落库、无重叠请求、同键恢复、失败阈值、401/403/409/本地错误和 active 阻止新领取。
- QtTest 覆盖默认 Tab、固定 Tab 状态、按钮禁用原因、记录倒序、单击/双击/Enter/按钮、详情切换、Esc 层级与焦点恢复。
- 配置测试覆盖:首次无凭据时空 token 拒绝且零 T-303 写入;已有凭据时空 token 原样保留既有 DPAPI 密文;非空 token 只替换一次且成功后清空输入;失败保留输入并聚焦首个错误;界面无 token 回读和清除/删除命令,日志与异常均无 token。
- 配置页不存在连接检查命令;保存、本地校验以及 consumer 未注入时点击禁用的开始入口均产生零 HTTP health、零 `claim-next`/`renew`/`evidence`、零 ADB/uiautomator2/PDD,并显示延后验证提示。静态检查不得出现网络或 ADB 探测调用链。
- compact/medium/wide 不重建模型或丢选择;长文案、100%–200% DPI、浅色/深色/高对比与纯键盘行为可人工复核。
- 应用独立启动、切换 Tab、Esc、查看记录与关闭窗口均产生零 claim、零 release、零 ADB;代码不导入 PDD 点击、数量、确认页、围栏、提交或付款能力。
- client 全量 unittest、compileall、wheel metadata、根目录完整 init、Vikunja 导出、上下文校验与 diff-check 通过。
### T-303 metadata-only 配置摘要与 Stop epoch 补充
1. 允许精确扩展 localstate/models.py、store.py 与对应 test_store.py,只增加 metadata-only profile summary/settings 读取;不得调用 DPAPI 或返回 token/密文。
2. Stop 必须提升 epoch/latch,使旧 timer dispatch 失效;飞行中结果先 durable commit,再按 CLAIMED/EMPTY/错误进入安全停止或恢复态。
3. 存在 pending/active 时冻结全部非 token 配置,只允许同 device id 替换 token;关闭窗口不得 terminate 正在 I/O 的 QThread,须等待有界返回与落库。
## 执行记录
### 2026-08-04T13:35:53Z · ila
2026-08-04 已按确认原型与 Windows UI/UX 规范落成任务:采用固定双 Tab、稳定 ID 的主从工作区、非模态持久错误、Esc 只退出记录详情、显式配置保存,以及停止仅阻止下一次领取的会话边界。独立运行时未注入单趟执行器,开始轮询必须禁用,确保零真实 claim、零 ADB;依赖 T-303、T-006 完成后再开工。
### 2026-08-04T17:56:06Z · ila
2026-08-05 T-304 代码实现与自动门禁已完成,任务继续保持 DOING,等待 needs_human_review 的原生 Windows 人工复核。
实现提交:42d52c8638f3d7fb3b500749ec2c9dd66d14303f;审计修复提交:73c438710bd975176d50166631a7ea2f6dbdc13f。已落地固定双 Tab、无真实 consumer 时禁领、长期 QThread worker 与 QTimer 调度、重启恢复与 Stop epoch/latch、严格歧义重放白名单、无秘密 UI DTO、配置保存/冻结语义、非模态记录详情和 Esc 返回。
第一轮 fixed-commit 审计发现并修复:65 位 token 被控件静默截断、consumer 无法获得本次 Start 冻结配置、metadata-only summary 对损坏密文/存储配置未充分失败闭合。新增首次及替换 token 零写入、consumer 趟内配置快照、空 BLOB/错误 storage class/损坏 URL、transport、range 且零 unprotect 回归。第二轮独立终审对 73c4387 结论为 PASS。
验证:聚焦 41 tests PASS;client 全量 229 tests PASS;compileall PASS;wheel build 与 verify_wheel_metadata PASS;仓库根 init.ps1 PASS(admin test/vet/build、client 229 tests、compileall、agent-context validator)。Qt offscreen 环境缺少可用字体目录,结构与交互截图已检查,但中文字体、DPI、主题、高对比度和纯键盘体验仍须在原生 Windows 人工验收,因此 done=false。
### 2026-08-04T18:02:15Z · ila
2026-08-05 主脑已将 T-304 合并到 main 并完成合并后验证。main 最终代码 SHA:3a27225;其中 UI 主提交:dfd88c3。
合并后仓库根 init.ps1 PASS;client 全量 232 tests PASS。设置 QT_QPA_FONTDIR=C:\Windows\Fonts 后完成离线截图复核,中文字与整体结构均可读,主脑视觉结构复核结论为 PASS。
当前仅完成自动化门禁与离线视觉结构复核。原生 Windows 下的 DPI、浅色/深色主题、高对比度、Narrator 以及纯键盘体验仍需人工验收,因此任务继续保持 DOING,Vikunja 保持 done=false 与 Doing。
### 2026-08-05T01:19:13Z · ila
2026-08-05 T-304 完整套件偶发测试修复:
- 完整测试套件暴露 `test_stop_during_claim_commits_then_requires_recovery_without_consumer_delivery` 与 FakeGateway 旧 2 秒等待位于同一时间边界的竞态;这是测试同步问题,不是生产轮询语义变化。
- 提交 `c9b39a4` 仅在测试替身增加 worker `entered` event,并把 gate 改为 5 秒有界、超时显式失败;未修改生产代码。测试继续验证重复 dispatch、Stop epoch/latch、旧 epoch 拒绝、consumer 零投递与 durable recovery。
- 独立审计 PASS;polling module 13 tests PASS,client 全量 233 tests PASS,仓库根 `init.ps1` PASS。
- 原生 Windows 人工 UI 验收仍未完成;T-304 保持 done=false / Doing 与 needs_human_review。
<!-- END VIKUNJA EXPORT -->
## 边界
- 本任务只能消费 T-303 暴露的 profile/session/recovery API,不得另建 SQLite、DPAPI、mutex、
HTTP task source 或第二套 active-claim 状态源。配置或恢复状态损坏必须失败闭合,不能用界面默认值
覆盖持久业务事实。
- 独立应用在未注入单趟 execution consumer 时必须禁用“开始轮询”,且启动、切换 Tab、Esc、查看记录、
保存配置和关闭窗口都必须产生零 claim、零 ADB。不能为了演示界面领取真实任务,也不能填充会被当作
真实状态的假记录。
- 停止轮询只阻止下一次新领取。不得取消已发出的 claim、清除 pending/active、release/abandon、
生成新 session 或幂等键、自动转领,也不得触发真机 Back、退出页面或其他设备动作;返回的 claim
必须先由 T-303 原子落库再更新 UI。
- 重启不自动开始轮询,不自动恢复任何真机点击。pending claim 只能用原 session/request 恢复;active
claim 只显示“待安全恢复”并阻止新领取,租约过期或 409 交 T-207 人工处理。
- 配置页只允许必填、格式、范围以及本地路径形态/存在性校验,不得提供或隐藏任何网络/ADB“连接检查”;
保存、校验都不得调用 HTTP health、`claim-next`、`renew`、`evidence`,也不得执行 ADB、uiautomator2
或拼多多探测。界面必须明确提示“服务身份将在首次真实领取时验证;设备与 App 状态由后续已取证执行
能力验证”,本地校验通过不能显示服务、设备或 App 已就绪。
- 服务地址只读为精确 `http://127.0.0.1:8080`。首次保存且没有既有 DPAPI 凭据时 token 必填;已有
凭据时空输入表示原样保留现有密文,非空输入才通过 T-303 的原子 profile API 替换,绝不能用空值
覆盖或删除。保存成功后清空 token 输入框且永不回填;保存失败保留输入并聚焦首个错误。MVP 不提供
清除/删除凭据命令,token 不得进入日志或异常文本。
- Qt worker 不得直接访问 Widget,UI 回调不得丢弃迟到的 claim 业务结果。连续失败只允许用 T-303
已持久化的同一幂等键重放安全网络操作;`retryable` 绝不推导为页面点击、证据替换、下单或提交可重试。
- 记录表的双击仅是非破坏性详情入口,并必须有 Enter 和显式按钮等价路径。Esc 只在详情态返回当前任务,
不停止轮询、不关闭窗口、不释放 claim、不改变服务端状态;返回后按稳定记录 ID 恢复选择、滚动和焦点。
- 不导入或实现 PDD 页面判据、规格选择、数量、确认页、证据上传、事件、失败上报、提交围栏或结果接口;
不编写或引用点击“提交订单”的代码,不编写支付、免密支付、先用后付或任何扣款控件代码。
- T-303 的 `load_profile()` 会解密设备 token,配置页不得为了读取普通设置或“已保存”布尔状态调用它。
本任务只可在既有 localstate store 增加 metadata-only 的 profile summary/settings 只读 API:沿用同一
SQLite 表与事务,不调用 DPAPI、不返回密文或明文 token、不新增存储、mutex 或第二套状态源。
- Stop 必须提升会话 epoch/latch,先取消 timer 并使已经排队的旧 epoch dispatch 失效。飞行中领取仍等待
HTTP 自身有界结果并由 T-303 完成 durable commit:成功领取转 `RECOVERY_REQUIRED`,EMPTY 转
`STOPPED`,结果不明、401、409、协议或本地错误保留原槽并停止;只有下一次显式 Start 才可同 key 恢复。
关闭窗口同样不得 terminate 正在 I/O 的 QThread,必须等待其有界返回与落库后再结束 worker。