--- 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 --- ## 问题 / 背景 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。 ### 2026-08-05T04:45:40Z · ila 2026-08-05 T-011 方案 B 由 T-304 所有者就地完成 polling 测试提速:仅修改 `client/tests/polling/test_coordinator.py`。13 个测试名完全不变,无删除/合并/skip;Qt 事件泵量子 10ms→1ms,删除同步 BLOCKED 路径无意义的固定 20ms 等待,并用“timer 未激活 + scheduled epoch 为空”的更强终态断言替代固定 30ms 等待。root 复跑 13/13 PASS,4.160s→3.574s;client 全量仍 255/255,12.892s(≤15s),compileall/diff-check/agent-context 通过。未改生产代码、轮询状态机或安全语义。T-304 仍因原生 Windows DPI/主题/高对比/Narrator/纯键盘人工验收保持 Doing。 ## 边界 - 本任务只能消费 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。