diff --git a/docs/06-tasks.md b/docs/06-tasks.md index a750cb4..92b3f88 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -26,7 +26,7 @@ | T-003 | 建立 `init.ps1` 统一入口 | T-001, T-002 | 一条命令完成两端安装与基础验证并打印启动命令;未配置时主动失败而不是静默跳过 | | T-004 | 建立核心数据模型与状态机 | T-001 | `tasks`、`order_authorizations`、`order_submissions` 表与 `04-architecture.md` 一致;状态流转有单元测试;金额用十进制字符串 | | T-005 | 网页端 MVP 交互原型 | - | `docs/design/web-*.html` 单文件假数据原型覆盖登录、表格工作台、创建弹窗、批量开始试选、路由化详情抽屉与完整详情;键盘、窄屏、空态 / 错误 / 加载、围栏后调和状态可演示;经人工确认前保持 `DOING` | -| T-006 | 桌面端 MVP 交互原型 | - | `docs/design/desk-*.html` 单文件假数据原型覆盖“采购工具”的采购执行 / 配置固定页签、顶部轮询控制、当前任务图片、滚动日志、时间倒序执行记录和详情弹窗;明确 dry-run / 真实下单、App 版本失配、围栏后不可重试;经人工确认前保持 `DOING` | +| T-006 | 桌面端 MVP 交互原型 | - | `docs/design/desk-*.html` 单文件假数据原型覆盖“采购工具”的采购执行 / 配置固定页签、顶部轮询控制、当前任务图片、滚动日志、时间倒序执行记录,以及左侧当前任务 / 记录详情双视图;明确 dry-run / 真实下单、App 版本失配、围栏后不可重试;经人工确认前保持 `DOING` | ### Phase 1 · 真机取证(最高风险,必须先做) diff --git a/docs/08-interaction-checklist.md b/docs/08-interaction-checklist.md index 0c59af7..90d3e89 100644 --- a/docs/08-interaction-checklist.md +++ b/docs/08-interaction-checklist.md @@ -110,7 +110,7 @@ IX 编号一经引用不再重用。交互清单不能扩大需求范围。 - 关联用户故事:US-003、US-008 | 关联需求:F-005、F-011 - 页面 / 组件:desk 采购执行页 `PollControls` + `DeviceStatusBar` + `CurrentTaskPanel` + - `ExecutionLog` + `ExecutionRecordTable` / `ExecutionRecordDialog` + `ExecutionLog` + `ExecutionRecordTable` + `ExecutionRecordDetailPanel` - 目标角色:采购执行员 - 前置条件:web 端连接正常;设备已通过连接检查 @@ -119,8 +119,8 @@ IX 编号一经引用不再重用。交互清单不能扩大需求范围。 1. 用户确认 web、设备和已取证 App 版本三项状态都就绪,在顶部状态区点击「开始轮询」。 2. 每到间隔时间领取一条任务,界面显示**这一趟是试选还是下单**、商品、规格、当前步骤。 3. 第一趟试选完成后**退出商品页释放手机**,继续下一轮轮询。 -4. 执行结果按时间倒序进入右侧两列表格;单击只选中,双击、Enter 或可见查看命令打开记录 - 详情,关闭后焦点回到原行。 +4. 执行结果按时间倒序进入右侧两列表格;单击只选中,双击、Enter 或可见查看命令在左侧打开 + 记录详情。宽屏可继续选择其他记录;可见返回按钮或 `Esc` 回到当前任务并恢复当前行焦点。 **状态与异常清单** @@ -130,7 +130,8 @@ IX 编号一经引用不再重用。交互清单不能扩大需求范围。 | 轮询中 | 显示下次轮询倒计时与连续失败计数 | | 执行中 | 显示当前趟次与步骤;**真机步骤期间禁用硬取消和关闭窗口** | | 当前任务图片 | 无可信图片时显示空态;有图时标来源与采集时间,不用其他来源图片凑合 | -| 执行记录详情 | 左侧原始文字、右侧图片证据、下方结构化采购结果;不提供下单、重试提交或付款动作 | +| 执行记录详情 | 左栏在当前任务 / 记录详情间切换,不打开模态框;详情内左侧原始文字、右侧图片证据、下方结构化采购结果;顶部实时状态不中断;不提供下单、重试提交或付款动作 | +| 详情读取异常 | 加载、失败、无图和记录不存在分别说明;仅可重试读取同一稳定记录 ID,过期异步结果不得覆盖当前视图 | | 无可领任务 | 显示「暂无待领任务」并继续下一轮,**不当作错误、不计入失败计数** | | 待人工 | 整页显著变色,说明缺什么、下一步做什么;该任务不再被本端领取 | | 连续失败 | 达到阈值自动停止轮询并显示原因,**不无限重试** | diff --git a/docs/current-state.md b/docs/current-state.md index fa68646..45ad89c 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -22,7 +22,7 @@ - 标准启动路径:`./init.ps1`(Windows)/ `./init.sh`。已存在但会因两端目录未初始化而以 退出码 3 结束并列出待办任务——这是预期行为 - 标准验证路径:目前只有 `python scripts/validate_agent_context.py` 可跑通 -- 当前 blocker:T-005 第二轮 Web 表格工作台和 T-006 第二轮“采购工具”桌面工作区均已通过 +- 当前 blocker:T-005 第二轮 Web 表格工作台和 T-006 第三轮“采购工具”桌面工作区均已通过 自动检查;两者经人工确认前不开始对应生产 UI。 ## 当前目录要点 @@ -43,11 +43,12 @@ - 已完成:无。 - 正在进行:T-005(网页端 MVP 交互原型)、T-006(桌面端 MVP 交互原型)。T-005 第二轮 - 已将三泳道改为传统表格;T-006 第二轮已改为“采购工具”的采购执行 / 配置固定页签,并增加 - 顶部轮询控制、当前任务图片、滚动日志、执行记录表格和详情弹窗。两项自动验证均已完成, + 已将三泳道改为传统表格;T-006 第三轮已把执行记录详情从模态框改为左侧内联双视图,支持 + 连续选行、可见返回按钮、`Esc` 层级、异常读取状态和紧凑宽度单视图。两项自动验证均已完成, 都因 `needs_human_review: true` 保持 `DOING`。 -- **下一步:人工浏览 6 个原型,重点确认 Web 表格与详情抽屉,以及桌面端顶部状态区、三块 - 布局、图片证据、两列执行记录和详情弹窗。** 明确确认后才能把 T-005 / T-006 改为 `DONE`。 +- **下一步:人工浏览 6 个原型,重点确认 Web 表格与详情抽屉,以及桌面端顶部状态区、图片 + 证据、两列执行记录和左侧内联详情 / 返回路径。** 明确确认后才能把 T-005 / T-006 改为 + `DONE`。 - 原型使用假数据、不调用真实接口、不驱动真机;T-001 / T-002 骨架可在确认后并行, Phase 2 生产页面还必须等待相关真机结论。 diff --git a/docs/design/desk-execution.html b/docs/design/desk-execution.html index acfae2a..a6b415a 100644 --- a/docs/design/desk-execution.html +++ b/docs/design/desk-execution.html @@ -168,7 +168,7 @@ main { max-width: 1480px; margin: 0 auto; padding: 16px; } .prototype-tools { display: grid; - grid-template-columns: minmax(240px, 1fr) auto; + grid-template-columns: minmax(240px, 1fr) minmax(180px, .55fr) auto; gap: 12px; align-items: end; margin-bottom: 12px; @@ -224,7 +224,9 @@ .banner p { margin: 0; color: var(--text-primary); } .banner button[hidden] { display: none; } .workspace { display: grid; grid-template-columns: minmax(0, 1.85fr) minmax(320px, 1fr); gap: 12px; margin-top: 12px; align-items: stretch; } + .left-pane { min-width: 0; min-height: 0; } .stack { display: grid; grid-template-rows: auto minmax(240px, 1fr); gap: 12px; min-width: 0; } + .stack[hidden], .detail-workspace[hidden] { display: none; } .card { border: 1px solid var(--stroke); border-radius: var(--radius-card); background: var(--surface-raised); box-shadow: 0 2px 8px rgba(0,0,0,.04); overflow: hidden; } .card-header { display: flex; align-items: start; justify-content: space-between; gap: 12px; padding: 14px 16px 10px; border-bottom: 1px solid var(--divider); } .card-header h1, .card-header h2 { margin: 0; font-size: 17px; line-height: 1.35; } @@ -296,11 +298,17 @@ .dialog-body p { margin: 0 0 10px; } .dialog-body code { overflow-wrap: anywhere; } .dialog-actions { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 20px; border-top: 1px solid var(--divider); } - .record-dialog { width: min(940px, calc(100vw - 32px)); } - .record-dialog header { display: flex; justify-content: space-between; gap: 12px; padding: 16px 20px 12px; border-bottom: 1px solid var(--divider); } - .record-dialog header h2 { margin: 0; font-size: 18px; } - .record-dialog header p { margin: 3px 0 0; color: var(--text-secondary); font-size: 12px; } - .record-dialog-grid { display: grid; grid-template-columns: minmax(0, 3fr) minmax(260px, 2fr); gap: 16px; padding: 16px 20px; } + .detail-workspace { min-height: 540px; height: 100%; display: flex; flex-direction: column; } + .detail-header { align-items: center; } + .detail-header-actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-wrap: wrap; } + .shortcut { margin-left: 5px; padding: 1px 5px; border: 1px solid var(--stroke); border-bottom-width: 2px; border-radius: 3px; color: var(--text-secondary); background: var(--surface-muted); font-size: 11px; } + .detail-scroll { min-height: 0; overflow: auto; flex: 1; } + .record-detail-grid { display: grid; grid-template-columns: minmax(0, 3fr) minmax(240px, 2fr); gap: 16px; padding: 16px 20px; } + .detail-feedback { margin: 16px 20px 0; padding: 16px; border: 1px solid var(--stroke); border-radius: 6px; background: var(--surface-muted); } + .detail-feedback[hidden] { display: none; } + .detail-feedback strong, .detail-feedback span { display: block; } + .detail-feedback span { margin-top: 4px; color: var(--text-secondary); } + .detail-feedback button { margin-top: 12px; } .detail-panel { min-width: 0; } .detail-panel h3, .record-result h3 { margin: 0 0 9px; font-size: 14px; } .detail-list { display: grid; grid-template-columns: 104px minmax(0, 1fr); gap: 7px 12px; margin: 0; } @@ -322,6 +330,7 @@ .workspace { grid-template-columns: 1fr; } .stack { grid-template-rows: auto auto; } .record-card { min-height: 480px; } + .workspace[data-mode="record"] .record-card { display: none; } .log-list { max-height: 220px; } } @media (max-width: 640px) { @@ -346,7 +355,9 @@ .log-tools, .record-actions { justify-content: flex-start; } .log-list li { grid-template-columns: 58px 1fr; } .log-list li span:last-child { grid-column: 1 / -1; } - .record-dialog-grid { grid-template-columns: 1fr; } + .record-detail-grid { grid-template-columns: 1fr; } + .detail-header-actions { align-items: stretch; justify-content: flex-start; } + .detail-header-actions .badge { white-space: normal; } .evidence-grid { grid-template-columns: 1fr; } .detail-list { grid-template-columns: 1fr; gap: 2px; } .detail-list dd { margin-bottom: 7px; } @@ -403,6 +414,14 @@ +
@@ -433,8 +452,9 @@ -
-
+
+
+

当前任务

尚未领取任务

@@ -482,6 +502,47 @@
    +
    + +
    @@ -496,7 +557,7 @@
    -

    单击选择;双击行、按 Enter 或使用“查看所选记录”打开详情。右键也可查看。

    +

    单击选择;双击行、按 Enter 或使用“查看所选记录”在左侧打开详情。详情态可连续选择其他记录。

    @@ -505,37 +566,6 @@
    - -
    -

    执行记录详情

    - -
    -
    -
    -

    原始文字

    -
    -
    任务编号
    -
    商品标题
    -
    目标规格
    -
    金额边界
    -
    提交围栏
    -
    原始说明
    -
    -
      -
      -
      -

      图片证据

      -
      -
      -
      -
      -

      采购结果

      -

      -

      -
      -
      -
      -

      安全说明

      @@ -593,6 +623,8 @@ }; let currentRecordId = records[0]?.id || null; let recordFocusReturn = null; + let workspaceMode = 'live'; + let detailRequestId = 0; const baseSteps = { idle: [], trial: ['打开商品(待真机判据)', '精确匹配颜色分类与尺码', '从规格面板读取单价', '截图并退出商品页'], @@ -671,13 +703,19 @@ } function selectRecord(id, focus = false) { currentRecordId = id; + let selectedRow = null; for (const row of $('record-rows').rows) { const selected = row.dataset.recordId === id; row.setAttribute('aria-selected', String(selected)); row.tabIndex = selected ? 0 : -1; + if (selected) selectedRow = row; if (selected && focus) row.focus(); } $('view-record').disabled = !records.some((record) => record.id === id); + if (workspaceMode === 'record') { + recordFocusReturn = selectedRow || recordFocusReturn; + renderRecordDetail(id); + } } function renderRecords() { const rows = records.map((record) => { @@ -724,19 +762,65 @@ const caption = document.createElement('figcaption'); caption.textContent = `${label} · 不访问真实拼多多`; figure.append(svg, caption); return figure; } - function openRecord(id, source) { - const record = records.find((item) => item.id === id); if (!record) return; - hideRecordMenu(); currentRecordId = id; recordFocusReturn = source || [...$('record-rows').rows].find((row) => row.dataset.recordId === id) || $('view-record'); + function showDetailFeedback(title, text, retry = false) { + $('detail-feedback-title').textContent = title; + $('detail-feedback-text').textContent = text; + $('retry-detail').hidden = !retry; + $('detail-feedback').hidden = false; + $('detail-content').hidden = true; + } + async function renderRecordDetail(id) { + const requestId = ++detailRequestId; + const prototypeState = $('detail-state').value; + const record = records.find((item) => item.id === id); + $('record-detail-pane').setAttribute('aria-busy', String(prototypeState === 'loading' || prototypeState === 'normal')); + if (prototypeState === 'loading') { + showDetailFeedback('正在读取执行记录', `正在按稳定记录 ID ${id} 读取文字、图片索引和采购结果。`); + return; + } + if (prototypeState === 'error') { + showDetailFeedback('执行记录加载失败', '当前任务与右侧列表不受影响。可安全重试读取同一记录,不会重新执行采购。', true); + $('record-detail-pane').setAttribute('aria-busy', 'false'); + return; + } + if (prototypeState === 'missing' || !record) { + showDetailFeedback('执行记录已不存在', '记录可能已归档或当前账号无权查看。请返回当前任务;不会用其他记录补齐内容。'); + $('record-detail-pane').setAttribute('aria-busy', 'false'); + return; + } + showDetailFeedback('正在读取执行记录', `正在加载 ${record.taskId} 的已保存证据。`); + await Promise.resolve(); + if (requestId !== detailRequestId || workspaceMode !== 'record' || currentRecordId !== id) return; const status = recordStatus[record.status] || {label:record.status}; - $('record-dialog-title').textContent = record.title; - $('record-dialog-meta').textContent = `${record.taskId} · ${formatRecordTime(record.collectedAt)} · ${status.label}`; + $('record-detail-title').textContent = record.title; + $('record-detail-meta').textContent = `${record.taskId} · ${formatRecordTime(record.collectedAt)} · ${status.label}`; $('detail-task-id').textContent = record.taskId; $('detail-product').textContent = record.title; $('detail-spec').textContent = record.spec; $('detail-cap').textContent = record.cap; $('detail-submission').textContent = record.submission; $('detail-note').textContent = record.note; $('detail-timeline').replaceChildren(...record.timeline.map((value) => { const li = document.createElement('li'); li.textContent = value; return li; })); if (record.evidence.length) $('detail-evidence').replaceChildren(...record.evidence.map(createEvidence)); else { const empty = document.createElement('div'); empty.className = 'preview-empty'; const strong = document.createElement('strong'); strong.textContent = '暂无可信图片'; empty.append(strong, '记录保留原始文字,不用其他图片凑合。'); $('detail-evidence').replaceChildren(empty); } $('detail-result').textContent = record.result; $('detail-result-note').textContent = record.resultNote; - $('record-dialog').showModal(); + $('detail-feedback').hidden = true; $('detail-content').hidden = false; + $('record-detail-pane').setAttribute('aria-busy', 'false'); + } + function openRecord(id, source) { + const record = records.find((item) => item.id === id); if (!record) return; + hideRecordMenu(); currentRecordId = id; + recordFocusReturn = source || [...$('record-rows').rows].find((row) => row.dataset.recordId === id) || $('view-record'); + workspaceMode = 'record'; $('workspace').dataset.mode = 'record'; + $('live-workspace').hidden = true; $('record-detail-pane').hidden = false; + renderRecordDetail(id); + $('return-live').focus(); + $('announcer').textContent = `已在左侧打开 ${record.title} 的执行记录详情。按 Escape 或返回当前任务按钮返回。`; + } + function showLiveWorkspace() { + if (workspaceMode !== 'record') return; + detailRequestId += 1; workspaceMode = 'live'; $('workspace').dataset.mode = 'live'; + $('record-detail-pane').hidden = true; $('live-workspace').hidden = false; + const currentRow = [...$('record-rows').rows].find((row) => row.dataset.recordId === currentRecordId); + const focusTarget = currentRow || (recordFocusReturn?.isConnected ? recordFocusReturn : $('view-record')); + focusTarget?.focus(); + $('announcer').textContent = '已返回当前任务;执行记录选择和列表位置已保留。'; } function render(key, announce = true) { const s = scenarios[key]; @@ -760,6 +844,7 @@ renderSteps(s.kind, s.active); $('countdown').textContent = s.countdown; $('failure-count').textContent = s.fail; $('completed-count').textContent = String(s.count || 0); $('session-state').textContent = s.session; $('current-step').textContent = s.step; + $('live-task-state').textContent = `实时:${s.session} · ${s.step}`; $('live-task-state').dataset.tone = s.tone; $('primary-action').textContent = sessionControl.label; $('primary-action').disabled = !sessionControl.enabled; $('action-reason').textContent = sessionControl.reason; $('context-action').hidden = !s.safeAction; $('context-action').textContent = s.safeAction ? s.action : ''; @@ -777,7 +862,7 @@ $('scenario').addEventListener('change', (e) => render(e.target.value)); $('theme').addEventListener('change', (e) => { document.documentElement.dataset.theme = e.target.value; $('announcer').textContent = `主题已切换为${e.target.options[e.target.selectedIndex].text}`; }); - $('reset').addEventListener('click', () => { $('scenario').value = 'idle'; $('logs').replaceChildren(); currentRecordId = records[0]?.id || null; renderRecords(); render('idle'); appendLog('状态', '假数据已复位。'); }); + $('reset').addEventListener('click', () => { showLiveWorkspace(); $('scenario').value = 'idle'; $('detail-state').value = 'normal'; $('logs').replaceChildren(); currentRecordId = records[0]?.id || null; renderRecords(); render('idle'); appendLog('状态', '假数据已复位。'); }); $('primary-action').addEventListener('click', () => { const next = $('scenario').value === 'idle' ? 'polling' : 'idle'; $('scenario').value = next; render(next); }); @@ -790,11 +875,16 @@ }); $('view-record').addEventListener('click', () => openRecord(currentRecordId, [...$('record-rows').rows].find((row) => row.dataset.recordId === currentRecordId))); $('record-menu-open').addEventListener('click', () => openRecord(currentRecordId, [...$('record-rows').rows].find((row) => row.dataset.recordId === currentRecordId))); - $('record-dialog-close').addEventListener('click', () => $('record-dialog').close()); - $('record-dialog-done').addEventListener('click', () => $('record-dialog').close()); - $('record-dialog').addEventListener('close', () => { if (recordFocusReturn?.isConnected) recordFocusReturn.focus(); }); + $('return-live').addEventListener('click', showLiveWorkspace); + $('detail-state').addEventListener('change', () => { if (workspaceMode === 'record') renderRecordDetail(currentRecordId); }); + $('retry-detail').addEventListener('click', () => { $('detail-state').value = 'normal'; renderRecordDetail(currentRecordId); }); document.addEventListener('pointerdown', (event) => { if (!$('record-menu').hidden && !$('record-menu').contains(event.target)) hideRecordMenu(); }); - document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && !$('record-menu').hidden) hideRecordMenu(); }); + document.addEventListener('keydown', (event) => { + if (event.key !== 'Escape') return; + if (!$('record-menu').hidden) { event.preventDefault(); hideRecordMenu(); return; } + if ($('info-dialog').open) return; + if (workspaceMode === 'record') { event.preventDefault(); showLiveWorkspace(); } + }); window.addEventListener('resize', hideRecordMenu); $('export-log').addEventListener('click', exportDemo); $('close-demo').addEventListener('click', () => { diff --git a/docs/routes.md b/docs/routes.md index 7206bf2..7be0dea 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -96,14 +96,16 @@ - 顶部状态区:web、设备、拼多多 App 实际 / 已取证版本和会话状态;开始 / 停止轮询固定在 最右侧且只控制轮询会话。版本不一致时 fail closed 并提示重新取证。 -- 宽屏三块工作区:左上当前任务,左下滚动日志,右侧执行记录;窄屏按此顺序改为单列。 +- 宽屏是主从工作区:左侧在“当前任务 + 滚动日志”和“执行记录详情”之间切换,右侧执行记录 + 保持可见;窄屏进入详情时暂时收起记录表,返回后恢复列表现场。 - 当前任务左侧显示商品、规格、数量、**当前是第一趟还是第二趟**、步骤和剩余租约;右侧约 1/3 显示可选商品 / 证据预览。无可信图片时保持空态,有图必须标来源与采集时间。 - 滚动日志标题区同时显示下次轮询、连续失败和本次完成,不再使用独立会话卡或底部控制条。 - 执行记录只显示标题、状态两列,任务编号与时间放在标题次行,按采集时间倒序。单击选择; - 双击、Enter、可见“查看所选记录”或右键打开详情。 -- 记录详情上部左侧显示原始文字、右侧显示图片证据,下部显示结构化采购结果;关闭后焦点 - 返回原行。图片缺失不使用其他来源凑合。 + 双击、Enter、可见“查看所选记录”或右键在左侧打开详情;详情态单击另一行直接更新详情。 +- 记录详情不是模态框:上部左侧显示原始文字、右侧显示图片证据,下部显示结构化采购结果; + 可见“返回当前任务”与 `Esc` 均能返回并恢复当前行焦点。加载失败、无图和记录不存在有明确 + 状态,图片缺失不使用其他来源凑合。 - **待人工时整页显著变色并说明缺什么**,不要让执行员盯着一个静止画面猜。 - 关闭窗口即停止轮询;连续失败达阈值自动停止并显示原因。 @@ -142,7 +144,7 @@ | `CurrentTaskPanel` | desk 采购执行页 | 当前任务文字、步骤与带来源的可选图片证据 | | `ExecutionLog` | desk 采购执行页 | 最新在上的滚动日志与假诊断导出 | | `ExecutionRecordTable` | desk 采购执行页 | 标题 / 状态两列、时间倒序和稳定记录 ID | -| `ExecutionRecordDialog` | desk 采购执行页 | 原始文字、图片证据、采购结果与焦点恢复 | +| `ExecutionRecordDetailPanel` | desk 采购执行页 | 左侧内联原始文字、图片证据、采购结果、读取状态与返回焦点 | ## 六、原型 diff --git a/docs/tasks/T-006.md b/docs/tasks/T-006.md index 55cc73f..20035c3 100644 --- a/docs/tasks/T-006.md +++ b/docs/tasks/T-006.md @@ -168,14 +168,49 @@ desk 端将使用 PySide6 / Qt Widgets,但当前没有可确认的桌面应用 ## 执行记录 -### 2026-08-03 · 第三轮内联详情待实现 +### 2026-08-03 · 第三轮左侧内联详情实现 -- 任务保持 `DOING`。本轮拟将执行记录详情从模态对话框改为左侧双视图工作区;只修改 T-006 - 写路径内的原型与同步文档,不扩展 API、真机或生产 PySide6 范围。 -- 计划验证双击 / Enter / 可见命令 / 右键入口、详情态连续选行、返回按钮、`Esc` 层级、焦点 - 恢复、紧凑宽度单视图,以及加载 / 失败 / 缺图 / 记录不存在状态。 -- PySide6 交接将把左栏映射为 `QStackedWidget`,整体映射为 `QSplitter`,右侧继续使用 - `QTableView` + 稳定 ID 模型;详情异步结果必须带请求代次并在 GUI 线程丢弃过期回调。 +- 任务继续保持 `DOING`:第三轮自动验证已经完成,但 `needs_human_review: true`,仍需人确认 + 工作区切换、返回路径、宽屏连续查看与紧凑窗口取舍。 +- 执行记录详情已从模态 `dialog` 移到左侧双视图工作区。默认 `LIVE_TASK` 保持当前任务和滚动 + 日志;双击、Enter、可见命令或右键进入 `RECORD_DETAIL(record_id)`,右侧宽屏列表不被遮挡。 +- 详情态单击或方向键选择另一行会按稳定记录 ID 更新左侧详情。可见“返回当前任务”按钮和 + `Esc` 共用返回命令,保留列表选择 / 滚动,并把焦点还给当前记录行。 +- `Esc` 先关闭右键菜单;信息对话框打开时不触发工作区返回。顶部连接、会话和安全 Banner + 始终保留,详情头部另显示实时会话摘要,证明查看历史不会冻结当前任务状态表达。 +- 原型控制器可演示详情正常、加载中、加载失败和记录不存在;缺图记录仍显示可信证据空态。 + 重新读取只读取同一记录,不执行采购。渲染带请求代次,切换记录或返回后丢弃旧异步结果。 +- `windows-ui-ux` 使本轮采用非模态主从工作区、可见返回命令、分层 Escape、稳定选择与焦点 + 恢复,并明确紧凑宽度采用单视图,而不是把两个高密度面板纵向无限堆叠。 + +**第三轮 HTML → PySide6 / Qt Widgets 交接增量** + +| 原型项 | 核心交互契约 | 推荐 Qt Widgets 映射 | 状态 / 线程要求 | +| --- | --- | --- | --- | +| 左侧双视图 | 当前任务 / 记录详情互斥;顶部状态不在切换区 | 左栏 `QStackedWidget`,整体 `QSplitter` | 页面切换不销毁当前任务模型、日志或列表状态 | +| 详情入口与连续选行 | 双击、Enter、按钮、右键共用命令;详情态换行立即换详情 | 共享 `QAction`;`QTableView.activated` 与按钮触发 | 记录身份使用模型中的稳定 ID,不使用代理行号 | +| 返回当前任务 | 可见按钮与 Escape 共用;恢复当前行焦点 | 共享返回 `QAction` + `QShortcut(Escape)` | 仅在无更高层浮层时启用;按稳定 ID 重新定位代理行 | +| 详情读取 | 加载 / 失败 / 缺图 / 不存在状态明确 | 状态页或 `QStackedWidget` + 只读详情控件 | worker 结果携带 record ID 与 request ID;GUI 线程拒绝过期结果 | +| 紧凑宽度 | 详情态暂时隐藏记录表,返回后恢复现场 | 响应窗口宽度调整 `QSplitter` 可见性 | 隐藏不重建模型,不丢选择、排序或滚动位置 | + +**第三轮已运行验证** + +- Node 静态检查:执行页 78 个 id 无重复,内联脚本可编译,执行记录详情 `dialog` 已不存在。 +- Playwright 在 1440 px 验证双击、Enter、可见按钮 / 右键共用内联详情;详情态连续选行会更新 + 标题,返回按钮与 `Esc` 恢复当前记录行焦点;右键菜单和信息对话框的 Escape 层级正确。 +- 详情正常、加载中、加载失败 / 重新读取、记录不存在、无图片均可演示;查看详情期间切换到 + 第一趟试选,顶部 Banner 和详情实时徽标继续更新,且详情不退出。 +- 375 / 768 / 1024 / 1440 px 均无页面级水平溢出;375 / 768 px 详情态隐藏记录表,返回后 + 恢复表格与唯一选择;1024 / 1440 px 保持宽屏主从布局。浏览器 0 error / 0 warning,页面无 + 外部资源请求。 +- `python scripts/validate_agent_context.py` 与 `git diff --check`:提交前运行并记录最终结果。 + +**第三轮未验证与待人工确认** + +- 没有生产 PySide6、Model/View、worker、图片持久化、API、ADB 或真机行为;浏览器原型不能 + 证明原生 Qt 焦点、Narrator、UI Automation、DPI、线程或性能。 +- 待人确认:宽屏查看详情时是否继续保留右表;紧凑宽度是否接受单视图;返回按钮位置、实时 + 状态徽标密度,以及详情加载 / 不存在文案。人工确认前任务不得改为 `DONE`。 ### 2026-08-03 · 第二轮采购工具工作区实现