feat(prototype): redesign desktop procurement workspace
This commit is contained in:
+1
-1
@@ -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 · 真机取证(最高风险,必须先做)
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ IX 编号一经引用不再重用。交互清单不能扩大需求范围。
|
||||
| IX-004 | US-002 | web `/tasks/{id}` 路由化详情抽屉 / 完整页 | 双击行、任务编号链接或直接进入 | 了解任务当前处境 | 从列表进入时右侧打开且保留现场;直达时显示完整页;按状态呈现唯一主区块与主动作 | P0 | 已定 |
|
||||
| IX-005 | US-004、US-005 | web 试选确认卡 | 查看后点确认 / 退回 | 确认机器选对了并授权 | 签发一次性授权并锁定单价,明确不付款 | P0 | 已定 |
|
||||
| IX-006 | US-006 | web 待付款核对卡 | 查看后手工标记 | 核对后付款 | 展示订单截图与授权信息;人付款后标记完成 | P0 | 已定 |
|
||||
| IX-007 | US-003 | desk 设备与参数页 | 填 serial → 连接检查 | 让设备就绪 | 连接成功并确认拼多多已安装 | P0 | 已定 |
|
||||
| IX-008 | US-003、US-008 | desk 采购执行页 | 开始 / 停止轮询 | 让手机自动跑并处理异常 | 待人工时显著提示缺什么;连续失败自动停 | P0 | 已定 |
|
||||
| IX-007 | US-003 | desk 配置页 | 填 serial → 连接检查 | 让设备就绪 | 连接成功并确认拼多多已安装 | P0 | 已定 |
|
||||
| IX-008 | US-003、US-008 | desk 采购执行页 | 顶部开始 / 停止轮询;查看执行记录 | 让手机自动跑并处理异常 | 三块工作区保留当前任务、滚动日志和时间倒序记录;待人工时显著提示缺什么;连续失败自动停 | P0 | 已定 |
|
||||
| IX-010 | US-005 | web 围栏前放弃授权入口 | 点击放弃 | 解开尚未提交的授权 | 授权作废,任务转待重新试选 | P0 | 已定 |
|
||||
| IX-011 | US-005、US-008 | web / desk 提交围栏与调和状态 | 真实点击前申请围栏;点击后回报 | 防止断网或重复操作产生第二笔订单 | 围栏前失败不点击;围栏后不明确只核查、不重试 | P0 | 已定 |
|
||||
| IX-012 | US-010 | web `/tasks` 表格选择与上下文批量栏 | 勾选后点「开始试选」 | 批量交给桌面端试选 | `DRAFT → PENDING` 原子提交;仅开始第一趟,不授权、不下单、不付款 | P0 | 已定 |
|
||||
@@ -109,23 +109,28 @@ IX 编号一经引用不再重用。交互清单不能扩大需求范围。
|
||||
### IX-008 桌面端定时轮询与人工接管
|
||||
|
||||
- 关联用户故事:US-003、US-008 | 关联需求:F-005、F-011
|
||||
- 页面 / 组件:desk 采购执行页 `PollControls` + `DeviceStatusBar`
|
||||
- 页面 / 组件:desk 采购执行页 `PollControls` + `DeviceStatusBar` + `CurrentTaskPanel` +
|
||||
`ExecutionLog` + `ExecutionRecordTable` / `ExecutionRecordDialog`
|
||||
- 目标角色:采购执行员
|
||||
- 前置条件:web 端连接正常;设备已通过连接检查
|
||||
|
||||
**正常路径**
|
||||
|
||||
1. 用户确认两个连接状态都为绿,点击「开始轮询」。
|
||||
1. 用户确认 web、设备和已取证 App 版本三项状态都就绪,在顶部状态区点击「开始轮询」。
|
||||
2. 每到间隔时间领取一条任务,界面显示**这一趟是试选还是下单**、商品、规格、当前步骤。
|
||||
3. 第一趟试选完成后**退出商品页释放手机**,继续下一轮轮询。
|
||||
4. 执行结果按时间倒序进入右侧两列表格;单击只选中,双击、Enter 或可见查看命令打开记录
|
||||
详情,关闭后焦点回到原行。
|
||||
|
||||
**状态与异常清单**
|
||||
|
||||
| 场景 | 本交互约定 |
|
||||
| --- | --- |
|
||||
| 默认 | 任一连接未就绪时「开始轮询」禁用并说明是哪一个 |
|
||||
| 默认 | 任一就绪条件不满足时「开始轮询」禁用,并说明是 web、设备还是 App 版本问题 |
|
||||
| 轮询中 | 显示下次轮询倒计时与连续失败计数 |
|
||||
| 执行中 | 显示当前趟次与步骤;**真机步骤期间禁用硬取消和关闭窗口** |
|
||||
| 当前任务图片 | 无可信图片时显示空态;有图时标来源与采集时间,不用其他来源图片凑合 |
|
||||
| 执行记录详情 | 左侧原始文字、右侧图片证据、下方结构化采购结果;不提供下单、重试提交或付款动作 |
|
||||
| 无可领任务 | 显示「暂无待领任务」并继续下一轮,**不当作错误、不计入失败计数** |
|
||||
| 待人工 | 整页显著变色,说明缺什么、下一步做什么;该任务不再被本端领取 |
|
||||
| 连续失败 | 达到阈值自动停止轮询并显示原因,**不无限重试** |
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
- 标准启动路径:`./init.ps1`(Windows)/ `./init.sh`。已存在但会因两端目录未初始化而以
|
||||
退出码 3 结束并列出待办任务——这是预期行为
|
||||
- 标准验证路径:目前只有 `python scripts/validate_agent_context.py` 可跑通
|
||||
- 当前 blocker:T-005 第二轮表格工作台修订和 T-006 均已通过自动检查;两者经人工确认前
|
||||
不开始对应生产 UI。
|
||||
- 当前 blocker:T-005 第二轮 Web 表格工作台和 T-006 第二轮“采购工具”桌面工作区均已通过
|
||||
自动检查;两者经人工确认前不开始对应生产 UI。
|
||||
|
||||
## 当前目录要点
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
| --- | --- | --- |
|
||||
| `docs/` | 已有 | 项目规范化文档,本次已完整生成 |
|
||||
| `docs/tasks/` | 已有(2 个活动任务) | T-005 / T-006 均为 `DOING`,含 `README.md` 与 `_template.md` |
|
||||
| `docs/design/` | 已有(6 个原型) | web 登录 / 建单 / 工作台 / 详情,desk 执行 / 设备参数;待人工确认 |
|
||||
| `docs/design/` | 已有(6 个原型) | web 登录 / 建单 / 工作台 / 详情,desk 采购执行 / 配置;待人工确认 |
|
||||
| `scripts/` | 已有 | `validate_agent_context.py` |
|
||||
| `web/` | **待建** | Go 后端与管理页面(T-001) |
|
||||
| `desk/` | **待建** | Python 桌面端(T-002) |
|
||||
@@ -43,10 +43,11 @@
|
||||
|
||||
- 已完成:无。
|
||||
- 正在进行:T-005(网页端 MVP 交互原型)、T-006(桌面端 MVP 交互原型)。T-005 第二轮
|
||||
已将三泳道改为传统表格,并增加创建弹窗、`DRAFT` 批量开始试选和路由化详情抽屉;两项
|
||||
自动验证均已完成。两个任务都因 `needs_human_review: true` 保持 `DOING`。
|
||||
- **下一步:人工浏览 6 个原型,重点确认 Web 表格字段、批量开始试选、创建弹窗、详情抽屉
|
||||
与安全文案,以及桌面端执行状态。** 明确确认后才能把 T-005 / T-006 改为 `DONE`。
|
||||
已将三泳道改为传统表格;T-006 第二轮已改为“采购工具”的采购执行 / 配置固定页签,并增加
|
||||
顶部轮询控制、当前任务图片、滚动日志、执行记录表格和详情弹窗。两项自动验证均已完成,
|
||||
都因 `needs_human_review: true` 保持 `DOING`。
|
||||
- **下一步:人工浏览 6 个原型,重点确认 Web 表格与详情抽屉,以及桌面端顶部状态区、三块
|
||||
布局、图片证据、两列执行记录和详情弹窗。** 明确确认后才能把 T-005 / T-006 改为 `DONE`。
|
||||
- 原型使用假数据、不调用真实接口、不驱动真机;T-001 / T-002 骨架可在确认后并行,
|
||||
Phase 2 生产页面还必须等待相关真机结论。
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="data:,">
|
||||
<title>cmbuyer 桌面端原型 · 设备与参数</title>
|
||||
<title>采购工具 · 配置原型</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
@@ -135,10 +135,10 @@
|
||||
<body>
|
||||
<div class="prototype-banner" role="note">PROTOTYPE - 仅供枚举交互,非实现依据;全部是假数据,不执行连接检查或保存真实配置</div>
|
||||
<header class="app-bar">
|
||||
<div class="brand">cmbuyer 执行器<small>Windows / PySide6 前置原型</small></div>
|
||||
<div class="brand">采购工具<small>Windows / PySide6 前置原型</small></div>
|
||||
<nav class="tabs" aria-label="主要页面">
|
||||
<a href="desk-execution.html" id="execution-link">采购执行</a>
|
||||
<a href="desk-device-settings.html" aria-current="page">设备与参数</a>
|
||||
<a href="desk-device-settings.html" aria-current="page">配置</a>
|
||||
</nav>
|
||||
<div class="app-actions"><label for="theme">主题</label><select id="theme" aria-label="原型主题"><option value="system">跟随系统</option><option value="light">浅色</option><option value="dark">深色</option></select></div>
|
||||
</header>
|
||||
|
||||
+337
-61
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="data:,">
|
||||
<title>cmbuyer 桌面端原型 · 采购执行</title>
|
||||
<title>采购工具 · 采购执行原型</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
@@ -182,7 +182,7 @@
|
||||
.tool-buttons { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.status-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(260px, .9fr);
|
||||
border: 1px solid var(--stroke);
|
||||
border-radius: var(--radius-card);
|
||||
background: var(--surface-raised);
|
||||
@@ -190,18 +190,25 @@
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.health { display: grid; grid-template-columns: auto 1fr; gap: 4px 9px; padding: 12px 16px; border-right: 1px solid var(--divider); }
|
||||
.health:last-child { border-right: 0; }
|
||||
.health:nth-child(3) { border-right: 1px solid var(--divider); }
|
||||
.health-mark { width: 10px; height: 10px; border-radius: 50%; background: currentColor; margin-top: 5px; }
|
||||
.health strong { font-weight: 650; }
|
||||
.health span:last-child { grid-column: 2; color: var(--text-secondary); font-size: 12px; }
|
||||
.session-control { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 12px; align-items: center; padding: 10px 12px; }
|
||||
.session-copy { min-width: 0; }
|
||||
.session-copy span { display: block; color: var(--text-secondary); font-size: 11px; }
|
||||
.session-copy strong { display: block; overflow-wrap: anywhere; }
|
||||
.session-control .primary { min-width: 112px; }
|
||||
.disabled-reason { grid-column: 1 / -1; color: var(--text-secondary); font-size: 11px; }
|
||||
.tone-success { color: var(--success); }
|
||||
.tone-info { color: var(--info); }
|
||||
.tone-caution { color: var(--caution); }
|
||||
.tone-critical { color: var(--critical); }
|
||||
.banner {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid currentColor;
|
||||
@@ -215,8 +222,9 @@
|
||||
.banner-symbol { font-weight: 800; }
|
||||
.banner strong { display: block; margin-bottom: 2px; }
|
||||
.banner p { margin: 0; color: var(--text-primary); }
|
||||
.workspace { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(320px, .7fr); gap: 12px; margin-top: 12px; }
|
||||
.stack { display: grid; gap: 12px; align-content: start; }
|
||||
.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; }
|
||||
.stack { display: grid; grid-template-rows: auto minmax(240px, 1fr); gap: 12px; min-width: 0; }
|
||||
.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; }
|
||||
@@ -227,6 +235,8 @@
|
||||
.badge[data-tone="caution"] { color: var(--caution); background: var(--caution-soft); }
|
||||
.badge[data-tone="critical"] { color: var(--critical); background: var(--critical-soft); }
|
||||
.card-body { padding: 14px 16px 16px; }
|
||||
.task-layout { display: grid; grid-template-columns: minmax(0, 2fr) minmax(180px, 1fr); gap: 16px; align-items: start; }
|
||||
.task-copy { min-width: 0; }
|
||||
.task-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 20px; margin: 0; }
|
||||
.task-grid div { min-width: 0; }
|
||||
.task-grid dt { color: var(--text-secondary); font-size: 12px; }
|
||||
@@ -239,31 +249,79 @@
|
||||
.steps li.active { color: var(--text-primary); font-weight: 650; }
|
||||
.steps li.active .step-mark { border-color: var(--accent); box-shadow: inset 0 0 0 4px var(--accent-soft); }
|
||||
.steps small { color: var(--text-secondary); font-weight: 400; }
|
||||
.metric-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
|
||||
.metric { padding: 10px; border: 1px solid var(--divider); border-radius: 6px; background: var(--surface-muted); }
|
||||
.metric span { display: block; color: var(--text-secondary); font-size: 11px; }
|
||||
.metric strong { display: block; margin-top: 2px; font-size: 16px; font-variant-numeric: tabular-nums; }
|
||||
.log-list { list-style: none; padding: 0; margin: 0; max-height: 290px; overflow: auto; }
|
||||
.product-preview { margin: 0; min-width: 0; border: 1px solid var(--divider); border-radius: 6px; overflow: hidden; background: var(--surface-muted); }
|
||||
.preview-stage { aspect-ratio: 4 / 3; min-height: 148px; display: grid; place-items: center; padding: 12px; border-bottom: 1px solid var(--divider); }
|
||||
.preview-stage svg { width: 100%; height: 100%; max-height: 210px; }
|
||||
.preview-empty { color: var(--text-secondary); text-align: center; }
|
||||
.preview-empty strong { display: block; color: var(--text-primary); margin-bottom: 4px; }
|
||||
.product-preview figcaption { padding: 9px 10px; color: var(--text-secondary); font-size: 11px; }
|
||||
.product-preview figcaption span, .product-preview figcaption time { display: block; }
|
||||
.log-card { display: flex; min-height: 240px; flex-direction: column; }
|
||||
.log-tools { display: flex; align-items: center; justify-content: flex-end; gap: 10px; flex-wrap: wrap; }
|
||||
.inline-metrics { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.inline-metrics span { color: var(--text-secondary); font-size: 11px; white-space: nowrap; }
|
||||
.inline-metrics strong { color: var(--text-primary); font-variant-numeric: tabular-nums; }
|
||||
.log-list { list-style: none; padding: 0; margin: 0; min-height: 210px; max-height: 360px; overflow: auto; }
|
||||
.log-list li { display: grid; grid-template-columns: 64px 78px 1fr; gap: 8px; padding: 8px 16px; border-bottom: 1px solid var(--divider); }
|
||||
.log-list li:last-child { border-bottom: 0; }
|
||||
.log-list time { color: var(--text-secondary); font-variant-numeric: tabular-nums; }
|
||||
.log-level { font-size: 12px; font-weight: 650; }
|
||||
.poll-bar { margin-top: 12px; padding: 12px 16px; display: grid; grid-template-columns: minmax(160px, auto) 1fr auto; gap: 16px; align-items: center; border: 1px solid var(--stroke); border-radius: var(--radius-card); background: var(--surface-raised); box-shadow: var(--shadow); }
|
||||
.poll-summary { display: flex; gap: 20px; flex-wrap: wrap; }
|
||||
.poll-summary span { color: var(--text-secondary); font-size: 12px; }
|
||||
.poll-summary strong { color: var(--text-primary); font-variant-numeric: tabular-nums; }
|
||||
.disabled-reason { color: var(--text-secondary); font-size: 12px; max-width: 320px; }
|
||||
dialog { width: min(560px, calc(100vw - 32px)); border: 1px solid var(--stroke); border-radius: 8px; padding: 0; color: var(--text-primary); background: var(--surface-raised); box-shadow: 0 24px 80px rgba(0,0,0,.3); }
|
||||
.record-card { min-width: 0; min-height: 0; display: flex; flex-direction: column; }
|
||||
.record-actions { display: flex; align-items: center; gap: 8px; }
|
||||
.table-wrap { min-height: 420px; max-height: 720px; overflow: auto; flex: 1; }
|
||||
.record-table { width: 100%; border-collapse: separate; border-spacing: 0; table-layout: fixed; }
|
||||
.record-table th { position: sticky; top: 0; z-index: 1; padding: 9px 12px; color: var(--text-secondary); background: var(--surface-muted); border-bottom: 1px solid var(--stroke); text-align: left; font-size: 12px; }
|
||||
.record-table th:last-child { width: 112px; }
|
||||
.record-table td { padding: 10px 12px; border-bottom: 1px solid var(--divider); vertical-align: top; }
|
||||
.record-table tbody tr { cursor: default; }
|
||||
.record-table tbody tr:hover { background: var(--surface-muted); }
|
||||
.record-table tbody tr[aria-selected="true"] { background: var(--accent-soft); }
|
||||
.record-table tbody tr:focus-visible { position: relative; outline-offset: -3px; }
|
||||
.record-title { display: block; font-weight: 650; overflow-wrap: anywhere; }
|
||||
.record-meta { display: block; margin-top: 3px; color: var(--text-secondary); font-size: 11px; font-variant-numeric: tabular-nums; }
|
||||
.record-status { display: inline-flex; padding: 2px 7px; border: 1px solid currentColor; border-radius: 999px; font-size: 11px; font-weight: 650; }
|
||||
.record-status[data-tone="success"] { color: var(--success); }
|
||||
.record-status[data-tone="info"] { color: var(--info); }
|
||||
.record-status[data-tone="caution"] { color: var(--caution); }
|
||||
.record-status[data-tone="critical"] { color: var(--critical); }
|
||||
.table-empty { padding: 28px 16px; color: var(--text-secondary); text-align: center; }
|
||||
.table-help { margin: 0; padding: 9px 12px; color: var(--text-secondary); border-top: 1px solid var(--divider); font-size: 11px; }
|
||||
.context-menu { position: fixed; z-index: 20; min-width: 150px; padding: 4px; border: 1px solid var(--stroke); border-radius: 6px; background: var(--surface-raised); box-shadow: var(--shadow); }
|
||||
.context-menu[hidden] { display: none; }
|
||||
.context-menu button { width: 100%; justify-content: flex-start; border-color: transparent; }
|
||||
dialog { width: min(560px, calc(100vw - 32px)); max-height: calc(100vh - 32px); overflow: auto; border: 1px solid var(--stroke); border-radius: 8px; padding: 0; color: var(--text-primary); background: var(--surface-raised); box-shadow: 0 24px 80px rgba(0,0,0,.3); }
|
||||
dialog::backdrop { background: rgba(0,0,0,.42); }
|
||||
.dialog-body { padding: 20px; }
|
||||
.dialog-body h2 { margin: 0 0 8px; font-size: 18px; }
|
||||
.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-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; }
|
||||
.detail-list dt { color: var(--text-secondary); }
|
||||
.detail-list dd { margin: 0; overflow-wrap: anywhere; }
|
||||
.timeline { margin: 14px 0 0; padding-left: 20px; color: var(--text-secondary); }
|
||||
.timeline li + li { margin-top: 5px; }
|
||||
.evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
||||
.evidence-card { margin: 0; border: 1px solid var(--divider); border-radius: 6px; overflow: hidden; background: var(--surface-muted); }
|
||||
.evidence-card:only-child { grid-column: 1 / -1; }
|
||||
.evidence-card svg { display: block; width: 100%; aspect-ratio: 4 / 3; }
|
||||
.evidence-card figcaption { padding: 8px 10px; color: var(--text-secondary); font-size: 11px; }
|
||||
.record-result { padding: 14px 20px 18px; border-top: 1px solid var(--divider); }
|
||||
.result-value { margin: 0; padding: 12px; border: 1px solid var(--divider); border-radius: 6px; background: var(--surface-muted); font-weight: 650; overflow-wrap: anywhere; }
|
||||
.result-note { margin: 8px 0 0; color: var(--text-secondary); font-size: 12px; }
|
||||
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
||||
|
||||
@media (max-width: 1007px) {
|
||||
.workspace { grid-template-columns: 1fr; }
|
||||
.stack { grid-template-rows: auto auto; }
|
||||
.record-card { min-height: 480px; }
|
||||
.log-list { max-height: 220px; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
@@ -275,21 +333,30 @@
|
||||
.prototype-tools { grid-template-columns: 1fr; }
|
||||
.status-strip { grid-template-columns: 1fr; }
|
||||
.health { border-right: 0; border-bottom: 1px solid var(--divider); }
|
||||
.health:last-child { border-bottom: 0; }
|
||||
.health:nth-child(3) { border-right: 0; }
|
||||
.session-control { grid-template-columns: 1fr; }
|
||||
.session-control .primary { width: 100%; min-height: 40px; }
|
||||
.banner { grid-template-columns: auto minmax(0, 1fr); }
|
||||
.banner button { grid-column: 1 / -1; width: 100%; }
|
||||
.task-layout { grid-template-columns: 1fr; }
|
||||
.task-grid { grid-template-columns: 1fr; }
|
||||
.task-grid .span-2 { grid-column: auto; }
|
||||
.metric-row { grid-template-columns: 1fr; }
|
||||
.preview-stage { aspect-ratio: 16 / 9; }
|
||||
.card-header { align-items: stretch; flex-direction: column; }
|
||||
.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; }
|
||||
.poll-bar { grid-template-columns: 1fr; gap: 8px; }
|
||||
.poll-bar button { width: 100%; min-height: 40px; }
|
||||
.record-dialog-grid { grid-template-columns: 1fr; }
|
||||
.evidence-grid { grid-template-columns: 1fr; }
|
||||
.detail-list { grid-template-columns: 1fr; gap: 2px; }
|
||||
.detail-list dd { margin-bottom: 7px; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
|
||||
}
|
||||
@media (forced-colors: active) {
|
||||
.prototype-banner, .banner, .badge, .primary, .health-mark { forced-color-adjust: auto; }
|
||||
.card, .status-strip, .poll-bar, .prototype-tools { box-shadow: none; }
|
||||
.card, .status-strip, .prototype-tools { box-shadow: none; }
|
||||
.health-mark { border: 1px solid ButtonText; }
|
||||
}
|
||||
</style>
|
||||
@@ -297,10 +364,10 @@
|
||||
<body>
|
||||
<div class="prototype-banner" role="note">PROTOTYPE - 仅供枚举交互,非实现依据;全部是假数据,不连接 ADB、web 服务或拼多多</div>
|
||||
<header class="app-bar">
|
||||
<div class="brand">cmbuyer 执行器<small>Windows / PySide6 前置原型</small></div>
|
||||
<div class="brand">采购工具<small>Windows / PySide6 前置原型</small></div>
|
||||
<nav class="tabs" aria-label="主要页面">
|
||||
<a href="desk-execution.html" aria-current="page">采购执行</a>
|
||||
<a href="desk-device-settings.html">设备与参数</a>
|
||||
<a href="desk-device-settings.html">配置</a>
|
||||
</nav>
|
||||
<div class="app-actions">
|
||||
<label for="theme">主题</label>
|
||||
@@ -352,11 +419,18 @@
|
||||
<div class="health" id="version-health">
|
||||
<span class="health-mark" aria-hidden="true"></span><strong>拼多多版本</strong><span>实际 / 已取证:均待 T-103 真机取证</span>
|
||||
</div>
|
||||
<div class="session-control" aria-label="轮询会话控制">
|
||||
<div class="session-copy"><span>会话状态</span><strong id="session-state">未启动</strong></div>
|
||||
<button type="button" class="primary" id="primary-action">开始轮询</button>
|
||||
<div class="session-copy"><span>当前步骤</span><strong id="current-step">等待开始</strong></div>
|
||||
<div class="disabled-reason" id="action-reason">web、设备和已取证 App 版本均就绪后才能开始。</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="banner" id="state-banner" data-tone="success" aria-live="polite" aria-atomic="true">
|
||||
<span class="banner-symbol" aria-hidden="true">i</span>
|
||||
<div><strong id="banner-title">设备已就绪</strong><p id="banner-text">可以开始定时轮询。当前页面不会连接真实设备。</p></div>
|
||||
<button type="button" id="context-action" hidden>查看说明</button>
|
||||
</section>
|
||||
|
||||
<div class="workspace">
|
||||
@@ -367,43 +441,101 @@
|
||||
<span class="badge" id="leg-badge" data-tone="success">空闲</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="task-grid">
|
||||
<div><dt>任务编号</dt><dd id="task-id">—</dd></div>
|
||||
<div><dt>剩余租约</dt><dd id="lease">—</dd></div>
|
||||
<div class="span-2"><dt>商品</dt><dd id="product">暂无商品;轮询会在安全就绪后领取一条任务</dd></div>
|
||||
<div><dt>颜色分类</dt><dd id="color">—</dd></div>
|
||||
<div><dt>尺码 / 数量</dt><dd id="size-qty">—</dd></div>
|
||||
<div><dt>授权单价</dt><dd id="auth-price">—</dd></div>
|
||||
<div><dt>金额上限</dt><dd id="price-cap">—</dd></div>
|
||||
<div class="span-2"><dt>提交围栏</dt><dd id="submission">未建立</dd></div>
|
||||
</dl>
|
||||
<ol class="steps" id="steps" aria-label="执行步骤"></ol>
|
||||
<div class="task-layout">
|
||||
<div class="task-copy">
|
||||
<dl class="task-grid">
|
||||
<div><dt>任务编号</dt><dd id="task-id">—</dd></div>
|
||||
<div><dt>剩余租约</dt><dd id="lease">—</dd></div>
|
||||
<div class="span-2"><dt>商品</dt><dd id="product">暂无商品;轮询会在安全就绪后领取一条任务</dd></div>
|
||||
<div><dt>颜色分类</dt><dd id="color">—</dd></div>
|
||||
<div><dt>尺码 / 数量</dt><dd id="size-qty">—</dd></div>
|
||||
<div><dt>授权单价</dt><dd id="auth-price">—</dd></div>
|
||||
<div><dt>金额上限</dt><dd id="price-cap">—</dd></div>
|
||||
<div class="span-2"><dt>提交围栏</dt><dd id="submission">未建立</dd></div>
|
||||
</dl>
|
||||
<ol class="steps" id="steps" aria-label="执行步骤"></ol>
|
||||
</div>
|
||||
<figure class="product-preview" aria-labelledby="preview-caption">
|
||||
<div class="preview-stage">
|
||||
<div class="preview-empty" id="preview-empty"><strong>暂无商品预览</strong>只有取得可信图片证据后才展示</div>
|
||||
<svg id="product-preview-image" viewBox="0 0 320 240" role="img" aria-label="原型假商品预览,不是真实拼多多图片" hidden>
|
||||
<rect width="320" height="240" fill="#e7f3ff"/>
|
||||
<rect x="28" y="24" width="264" height="192" rx="12" fill="#ffffff" stroke="#9ac8ee" stroke-width="2"/>
|
||||
<path d="M114 72l30-18h32l30 18 33 12-18 43-23-10v61h-76v-61l-23 10-18-43 33-12z" fill="#f5f5f5" stroke="#65788a" stroke-width="3"/>
|
||||
<path d="M144 55c3 20 29 20 32 0" fill="none" stroke="#65788a" stroke-width="3"/>
|
||||
<text x="160" y="208" text-anchor="middle" fill="#3a5268" font-size="13" font-family="Segoe UI, sans-serif">内联假证据 · 不访问拼多多</text>
|
||||
</svg>
|
||||
</div>
|
||||
<figcaption id="preview-caption"><span id="preview-source">来源:尚无可信图片</span><time id="preview-time">采集时间:—</time></figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card" aria-labelledby="run-title">
|
||||
<div class="card-header"><div><h2 id="run-title">会话概览</h2><p>只显示当前前台会话,不代表后台进程</p></div></div>
|
||||
<div class="card-body metric-row">
|
||||
<div class="metric"><span>下次轮询</span><strong id="countdown">—</strong></div>
|
||||
<div class="metric"><span>连续失败</span><strong id="failure-count">0 / 3</strong></div>
|
||||
<div class="metric"><span>本次完成</span><strong id="completed-count">0</strong></div>
|
||||
<section class="card log-card" aria-labelledby="log-title">
|
||||
<div class="card-header">
|
||||
<div><h2 id="log-title">滚动日志</h2><p>最新事件在上;不含凭据、地址、手机号或页面全文</p></div>
|
||||
<div class="log-tools">
|
||||
<div class="inline-metrics" aria-label="会话指标"><span>下次轮询 <strong id="countdown">—</strong></span><span>连续失败 <strong id="failure-count">0 / 3</strong></span><span>本次完成 <strong id="completed-count">0</strong></span></div>
|
||||
<button type="button" id="export-log">导出假诊断摘要</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="log-list" id="logs" aria-live="polite" aria-relevant="additions"></ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="card" aria-labelledby="log-title">
|
||||
<div class="card-header"><div><h2 id="log-title">事件日志</h2><p>假日志;不含凭据、地址、手机号或页面全文</p></div><button type="button" id="export-log">导出假诊断摘要</button></div>
|
||||
<ul class="log-list" id="logs" aria-live="polite" aria-relevant="additions"></ul>
|
||||
<section class="card record-card" aria-labelledby="record-title">
|
||||
<div class="card-header">
|
||||
<div><h2 id="record-title">执行记录</h2><p><span id="record-count">0</span> 条假记录 · 按采集时间倒序</p></div>
|
||||
<div class="record-actions"><button type="button" id="view-record" disabled>查看所选记录</button></div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="record-table" aria-describedby="record-help">
|
||||
<thead><tr><th scope="col">标题</th><th scope="col">状态</th></tr></thead>
|
||||
<tbody id="record-rows"></tbody>
|
||||
</table>
|
||||
<div class="table-empty" id="record-empty" hidden>暂无执行记录。开始轮询并产生结果后会显示在这里。</div>
|
||||
</div>
|
||||
<p class="table-help" id="record-help">单击选择;双击行、按 Enter 或使用“查看所选记录”打开详情。右键也可查看。</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="poll-bar" aria-label="轮询控制">
|
||||
<button type="button" class="primary" id="primary-action">开始轮询</button>
|
||||
<div class="poll-summary"><span>会话:<strong id="session-state">未启动</strong></span><span>当前步骤:<strong id="current-step">等待开始</strong></span></div>
|
||||
<div class="disabled-reason" id="action-reason">web、设备和已取证 App 版本均就绪后才能开始。</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div class="context-menu" id="record-menu" role="menu" hidden>
|
||||
<button type="button" id="record-menu-open" role="menuitem">查看记录详情</button>
|
||||
</div>
|
||||
|
||||
<dialog class="record-dialog" id="record-dialog" aria-labelledby="record-dialog-title">
|
||||
<header>
|
||||
<div><h2 id="record-dialog-title">执行记录详情</h2><p id="record-dialog-meta"></p></div>
|
||||
<button type="button" id="record-dialog-close" aria-label="关闭执行记录详情">关闭</button>
|
||||
</header>
|
||||
<div class="record-dialog-grid">
|
||||
<section class="detail-panel" aria-labelledby="original-copy-title">
|
||||
<h3 id="original-copy-title">原始文字</h3>
|
||||
<dl class="detail-list">
|
||||
<dt>任务编号</dt><dd id="detail-task-id"></dd>
|
||||
<dt>商品标题</dt><dd id="detail-product"></dd>
|
||||
<dt>目标规格</dt><dd id="detail-spec"></dd>
|
||||
<dt>金额边界</dt><dd id="detail-cap"></dd>
|
||||
<dt>提交围栏</dt><dd id="detail-submission"></dd>
|
||||
<dt>原始说明</dt><dd id="detail-note"></dd>
|
||||
</dl>
|
||||
<ol class="timeline" id="detail-timeline" aria-label="执行时间线"></ol>
|
||||
</section>
|
||||
<section class="detail-panel" aria-labelledby="evidence-title">
|
||||
<h3 id="evidence-title">图片证据</h3>
|
||||
<div class="evidence-grid" id="detail-evidence"></div>
|
||||
</section>
|
||||
</div>
|
||||
<section class="record-result" aria-labelledby="result-title">
|
||||
<h3 id="result-title">采购结果</h3>
|
||||
<p class="result-value" id="detail-result"></p>
|
||||
<p class="result-note" id="detail-result-note"></p>
|
||||
</section>
|
||||
<div class="dialog-actions"><button type="button" class="primary" id="record-dialog-done">关闭详情</button></div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="info-dialog" aria-labelledby="dialog-title">
|
||||
<div class="dialog-body">
|
||||
<h2 id="dialog-title">安全说明</h2>
|
||||
@@ -423,6 +555,44 @@
|
||||
product: '纯棉圆领短袖 T 恤(原型假数据)',
|
||||
color: '白色', sizeQty: 'XL / 2 件', authPrice: 'CNY 32.50', cap: 'CNY 80.00'
|
||||
};
|
||||
const records = [
|
||||
{
|
||||
id:'REC-DEMO-0042', taskId:'CB-20260803-0042', title:'纯棉圆领短袖 T 恤(原型假数据)', collectedAt:'2026-08-03T13:52:18+08:00',
|
||||
status:'RECONCILIATION_REQUIRED', spec:'白色 | XL | 2 件', cap:'CNY 80.00', submission:'SUB-DEMO-0042 · 结果不明确',
|
||||
note:'围栏后网络中断,订单可能已创建。只允许核查同一提交记录,不能重试。', evidence:['商品页截图裁剪(内联假证据)','规格面板截图(内联假证据)'],
|
||||
timeline:['13:51:04 重新选择同一规格','13:51:41 三道价格闸门通过(假数据)','13:51:58 围栏已建立','13:52:18 点击结果不明确,转人工核查'],
|
||||
result:'下单待核查:白色 | XL | CNY 32.50 | 2 件', resultNote:'订单可能已创建、支付未完成。系统不会再次提交,也不会付款。'
|
||||
},
|
||||
{
|
||||
id:'REC-DEMO-0041', taskId:'CB-20260803-0041', title:'办公室桌面收纳盒(原型假数据)', collectedAt:'2026-08-03T12:24:09+08:00',
|
||||
status:'WAITING_CONFIRMATION', spec:'透明 | L | 2 件', cap:'CNY 60.00', submission:'未建立',
|
||||
note:'第一趟试选完成,规格面板证据已回传,等待管理员确认。', evidence:['商品页截图裁剪(内联假证据)','规格面板截图(内联假证据)'],
|
||||
timeline:['12:22:31 打开商品','12:23:04 匹配透明 / L','12:23:38 读取规格面板单价','12:24:09 截图并退出商品页'],
|
||||
result:'试选:透明 | L | CNY 24.00 | 2 件', resultNote:'这只是试选结果,不代表已下单。'
|
||||
},
|
||||
{
|
||||
id:'REC-DEMO-0038', taskId:'CB-20260803-0038', title:'标签打印纸补货(原型假数据)', collectedAt:'2026-08-03T10:06:42+08:00',
|
||||
status:'WAITING_PAYMENT', spec:'白色 | 50×30 | 1 件', cap:'CNY 50.00', submission:'SUB-DEMO-0038 · SUBMITTED',
|
||||
note:'已明确创建待付款订单,自动化已停止,等待人核对后自行付款。', evidence:['订单确认页非敏感截图(内联假证据)'],
|
||||
timeline:['10:04:22 重新选择同一规格','10:05:10 价格与数量复核通过','10:05:36 获得唯一围栏许可','10:06:42 明确创建待付款订单'],
|
||||
result:'已创建待付款订单:白色 | 50×30 | CNY 46.00 | 1 件', resultNote:'系统只创建待付款订单,付款始终由人完成。'
|
||||
},
|
||||
{
|
||||
id:'REC-DEMO-0032', taskId:'CB-20260803-0032', title:'仓库打包胶带(原型假数据)', collectedAt:'2026-08-03T09:18:05+08:00',
|
||||
status:'NEEDS_MANUAL', spec:'透明 | 45mm | 4 件', cap:'CNY 55.00', submission:'未建立',
|
||||
note:'商品页面未取得可信图片,授权价与当前规格面板价格不一致,已转人工。', evidence:[],
|
||||
timeline:['09:16:40 打开商品','09:17:22 匹配透明 / 45mm','09:18:05 价格闸门二不通过,停止自动流程'],
|
||||
result:'未采购:授权价 CNY 12.00 | 当前价 CNY 13.50', resultNote:'没有创建订单,也没有付款。'
|
||||
}
|
||||
].sort((a, b) => b.collectedAt.localeCompare(a.collectedAt));
|
||||
const recordStatus = {
|
||||
RECONCILIATION_REQUIRED:{label:'结果待核查',tone:'critical'},
|
||||
WAITING_CONFIRMATION:{label:'等待确认',tone:'caution'},
|
||||
WAITING_PAYMENT:{label:'待付款',tone:'success'},
|
||||
NEEDS_MANUAL:{label:'转人工',tone:'caution'}
|
||||
};
|
||||
let currentRecordId = records[0]?.id || null;
|
||||
let recordFocusReturn = null;
|
||||
const baseSteps = {
|
||||
idle: [],
|
||||
trial: ['打开商品(待真机判据)', '精确匹配颜色分类与尺码', '从规格面板读取单价', '截图并退出商品页'],
|
||||
@@ -447,7 +617,27 @@
|
||||
security: { tone:'critical', title:'检测到安全校验,已停止', text:'保留现场并转人工;不尝试绕过验证码、风控、人脸或短信校验。若已围栏,只调和同一提交。', web:'ok', device:'ok', version:'ok', leg:'待人工 · 安全校验', legTone:'critical', action:'打开人工核查说明', enabled:true, safeAction:'review', reason:'只显示核查说明,不提供绕过或继续入口。', session:'已停止', step:'等待人工处理', kind:'order', active:5, countdown:'—', fail:'1 / 3', submission:'SUB-DEMO-0042 · RECONCILIATION_REQUIRED' },
|
||||
payment_handoff: { tone:'critical', title:'进入外部支付交接,已停止', text:'订单可能已创建、支付未完成。不会读取或输入支付凭据,也没有“继续付款”按钮。', web:'ok', device:'ok', version:'ok', leg:'待人工 · 外部支付', legTone:'critical', action:'打开人工核查说明', enabled:true, safeAction:'paymentInfo', reason:'只显示人工核查与付款边界。', session:'已停止', step:'人工核查同一提交', kind:'order', active:5, countdown:'—', fail:'0 / 3', submission:'SUB-DEMO-0042 · HANDED_OFF' },
|
||||
manual: { tone:'caution', title:'任务已转人工:授权价与现价不一致', text:'授权 CNY 32.50,规格面板现价 CNY 35.00。没有下单,也不会自动放弃;请联系管理员处理。', web:'ok', device:'ok', version:'ok', leg:'NEEDS_MANUAL', legTone:'caution', action:'导出假诊断摘要', enabled:true, safeAction:'export', reason:'导出仅生成本地假文本,不包含页面全文或敏感信息。', session:'继续轮询其他任务', step:'当前任务已移交', kind:'order', active:1, countdown:'00:11', fail:'0 / 3', submission:'未建立' },
|
||||
failed_stop: { tone:'critical', title:'连续失败达到阈值,轮询已停止', text:'最近三次均为服务连接失败。检查设置和网络;修复后重新开始普通轮询,不会重试任何提交点击。', web:'bad', device:'ok', version:'ok', leg:'自动停止', legTone:'critical', action:'前往设备与参数', enabled:true, safeAction:'settings', reason:'先修复连接;不会自动恢复轮询。', session:'已停止', step:'等待修复', kind:'idle', active:-1, countdown:'—', fail:'3 / 3', count:4 }
|
||||
failed_stop: { tone:'critical', title:'连续失败达到阈值,轮询已停止', text:'最近三次均为服务连接失败。检查设置和网络;修复后重新开始普通轮询,不会重试任何提交点击。', web:'bad', device:'ok', version:'ok', leg:'自动停止', legTone:'critical', action:'前往配置', enabled:true, safeAction:'settings', reason:'先修复连接;不会自动恢复轮询。', session:'已停止', step:'等待修复', kind:'idle', active:-1, countdown:'—', fail:'3 / 3', count:4 }
|
||||
};
|
||||
const sessionControls = {
|
||||
not_ready:{label:'开始轮询',enabled:false,reason:'web 服务未就绪,不能领取任务。'},
|
||||
version_mismatch:{label:'开始轮询',enabled:false,reason:'App 版本失配,必须先完成本项目真机取证。'},
|
||||
idle:{label:'开始轮询',enabled:true,reason:'只启动普通轮询,不会直接执行下单。'},
|
||||
polling:{label:'停止轮询',enabled:true,reason:'停止后不再领取新任务。'},
|
||||
trial:{label:'停止轮询',enabled:false,reason:'真机步骤执行中,禁止硬停止。'},
|
||||
trial_done:{label:'停止轮询',enabled:true,reason:'手机已退出商品页,可以安全停止轮询。'},
|
||||
dry_run:{label:'停止轮询',enabled:false,reason:'只读演练执行中,禁止硬停止。'},
|
||||
dry_run_ready:{label:'停止轮询',enabled:true,reason:'演练已退出确认页,可以安全停止轮询。'},
|
||||
order_prefence:{label:'停止轮询',enabled:false,reason:'真实第二趟校验中,禁止硬停止。'},
|
||||
fencing:{label:'停止轮询',enabled:false,reason:'围栏申请中,禁止关闭、取消或重复申请。'},
|
||||
fence_failed:{label:'开始轮询',enabled:false,reason:'先核查围栏状态;不能用重新轮询绕过不明确结果。'},
|
||||
fenced:{label:'停止轮询',enabled:false,reason:'已建立围栏,只能调和同一提交。'},
|
||||
waiting_payment:{label:'开始轮询',enabled:false,reason:'先由人核对待付款订单并完成收口。'},
|
||||
uncertain:{label:'开始轮询',enabled:false,reason:'订单结果不明确,只能核查同一提交。'},
|
||||
security:{label:'开始轮询',enabled:false,reason:'安全校验已转人工,不允许自动继续。'},
|
||||
payment_handoff:{label:'开始轮询',enabled:false,reason:'外部支付交接中,自动化保持停止。'},
|
||||
manual:{label:'停止轮询',enabled:true,reason:'当前任务已移交,仍可安全停止普通轮询。'},
|
||||
failed_stop:{label:'开始轮询',enabled:false,reason:'先到配置页修复连接;不会自动恢复轮询。'}
|
||||
};
|
||||
|
||||
function setHealth(id, state, detail) {
|
||||
@@ -476,8 +666,81 @@
|
||||
li.append(time, badge, text); $('logs').prepend(li);
|
||||
while ($('logs').children.length > 8) $('logs').lastElementChild.remove();
|
||||
}
|
||||
function formatRecordTime(value) {
|
||||
return value.slice(5, 16).replace('T', ' ');
|
||||
}
|
||||
function selectRecord(id, focus = false) {
|
||||
currentRecordId = id;
|
||||
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 && focus) row.focus();
|
||||
}
|
||||
$('view-record').disabled = !records.some((record) => record.id === id);
|
||||
}
|
||||
function renderRecords() {
|
||||
const rows = records.map((record) => {
|
||||
const status = recordStatus[record.status] || {label:record.status,tone:'info'};
|
||||
const row = document.createElement('tr');
|
||||
row.dataset.recordId = record.id;
|
||||
row.setAttribute('aria-selected', 'false');
|
||||
const titleCell = document.createElement('td');
|
||||
const title = document.createElement('span'); title.className = 'record-title'; title.textContent = record.title;
|
||||
const meta = document.createElement('span'); meta.className = 'record-meta'; meta.textContent = `${record.taskId} · ${formatRecordTime(record.collectedAt)}`;
|
||||
titleCell.append(title, meta);
|
||||
const statusCell = document.createElement('td');
|
||||
const badge = document.createElement('span'); badge.className = 'record-status'; badge.dataset.tone = status.tone; badge.textContent = status.label;
|
||||
statusCell.append(badge); row.append(titleCell, statusCell);
|
||||
row.addEventListener('click', () => selectRecord(record.id));
|
||||
row.addEventListener('dblclick', (event) => { if (event.target.closest('button,a,input,select')) return; selectRecord(record.id); openRecord(record.id, row); });
|
||||
row.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter') { event.preventDefault(); openRecord(record.id, row); return; }
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
const rows = [...$('record-rows').rows]; const index = rows.indexOf(row); const delta = event.key === 'ArrowDown' ? 1 : -1;
|
||||
const next = rows[Math.max(0, Math.min(rows.length - 1, index + delta))]; if (next) selectRecord(next.dataset.recordId, true);
|
||||
}
|
||||
if (event.key === 'F10' && event.shiftKey) { event.preventDefault(); showRecordMenu(record.id, row.getBoundingClientRect().left + 16, row.getBoundingClientRect().top + 24); }
|
||||
});
|
||||
row.addEventListener('contextmenu', (event) => { event.preventDefault(); selectRecord(record.id); showRecordMenu(record.id, event.clientX, event.clientY); });
|
||||
return row;
|
||||
});
|
||||
$('record-rows').replaceChildren(...rows);
|
||||
$('record-count').textContent = String(records.length);
|
||||
$('record-empty').hidden = records.length > 0;
|
||||
if (records.length) selectRecord(records.some((record) => record.id === currentRecordId) ? currentRecordId : records[0].id);
|
||||
}
|
||||
function showRecordMenu(id, x, y) {
|
||||
currentRecordId = id;
|
||||
const menu = $('record-menu'); menu.hidden = false;
|
||||
menu.style.left = `${Math.min(x, innerWidth - 170)}px`; menu.style.top = `${Math.min(y, innerHeight - 54)}px`;
|
||||
$('record-menu-open').focus();
|
||||
}
|
||||
function hideRecordMenu() { $('record-menu').hidden = true; }
|
||||
function createEvidence(label) {
|
||||
const figure = document.createElement('figure'); figure.className = 'evidence-card';
|
||||
const svg = $('product-preview-image').cloneNode(true); svg.hidden = false; svg.removeAttribute('id'); svg.setAttribute('aria-label', `${label},原型内联假证据`);
|
||||
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');
|
||||
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}`;
|
||||
$('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();
|
||||
}
|
||||
function render(key, announce = true) {
|
||||
const s = scenarios[key];
|
||||
const sessionControl = sessionControls[key];
|
||||
$('state-banner').dataset.tone = s.tone;
|
||||
$('banner-title').textContent = s.title; $('banner-text').textContent = s.text;
|
||||
setHealth('web-health', s.web, s.web === 'bad' ? '不可达(模拟)· 已停止领取' : '已连接 · 假环境');
|
||||
@@ -491,35 +754,48 @@
|
||||
$('color').textContent = hasTask ? task.color : '—'; $('size-qty').textContent = hasTask ? task.sizeQty : '—';
|
||||
$('auth-price').textContent = hasTask && s.kind !== 'trial' ? task.authPrice : '—'; $('price-cap').textContent = hasTask ? task.cap : '—';
|
||||
$('submission').textContent = s.submission || '未建立';
|
||||
$('preview-empty').hidden = hasTask; $('product-preview-image').hidden = !hasTask;
|
||||
$('preview-source').textContent = hasTask ? '来源:商品页截图裁剪(内联假证据)' : '来源:尚无可信图片';
|
||||
$('preview-time').textContent = hasTask ? '采集时间:2026-08-03 13:51(假数据)' : '采集时间:—';
|
||||
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;
|
||||
$('primary-action').textContent = s.action; $('primary-action').disabled = !s.enabled; $('primary-action').dataset.safeAction = s.safeAction || '';
|
||||
$('action-reason').textContent = s.reason;
|
||||
$('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 : '';
|
||||
$('context-action').dataset.safeAction = s.safeAction || '';
|
||||
if (announce) { $('announcer').textContent = `${s.title}。${s.text}`; appendLog(s.tone === 'critical' ? '停止' : s.tone === 'caution' ? '注意' : s.tone === 'success' ? '完成' : '状态', `切换原型状态:${s.title}`); }
|
||||
}
|
||||
function showDialog(title, text) {
|
||||
$('dialog-title').textContent = title; $('dialog-text').textContent = text; $('info-dialog').showModal();
|
||||
}
|
||||
function exportDemo() {
|
||||
const blob = new Blob(['cmbuyer 原型假诊断摘要\n无真实任务、无凭据、无页面全文。\n'], {type:'text/plain;charset=utf-8'});
|
||||
const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'cmbuyer-prototype-diagnostic.txt'; a.click(); URL.revokeObjectURL(url);
|
||||
const blob = new Blob(['采购工具原型假诊断摘要\n无真实任务、无凭据、无页面全文。\n'], {type:'text/plain;charset=utf-8'});
|
||||
const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'purchase-tool-prototype-diagnostic.txt'; a.click(); URL.revokeObjectURL(url);
|
||||
appendLog('完成', '已生成本地假诊断摘要;没有外部网络请求。');
|
||||
}
|
||||
|
||||
$('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(); render('idle'); appendLog('状态', '假数据已复位。'); });
|
||||
$('reset').addEventListener('click', () => { $('scenario').value = 'idle'; $('logs').replaceChildren(); currentRecordId = records[0]?.id || null; renderRecords(); render('idle'); appendLog('状态', '假数据已复位。'); });
|
||||
$('primary-action').addEventListener('click', () => {
|
||||
const action = $('primary-action').dataset.safeAction;
|
||||
if (!action) {
|
||||
const next = $('scenario').value === 'idle' ? 'polling' : 'idle'; $('scenario').value = next; render(next); return;
|
||||
}
|
||||
const next = $('scenario').value === 'idle' ? 'polling' : 'idle'; $('scenario').value = next; render(next);
|
||||
});
|
||||
$('context-action').addEventListener('click', () => {
|
||||
const action = $('context-action').dataset.safeAction;
|
||||
if (action === 'settings') { location.href = 'desk-device-settings.html'; return; }
|
||||
if (action === 'export') { exportDemo(); return; }
|
||||
if (action === 'paymentInfo') { showDialog('人工付款边界', '自动化已经停止。本系统不会点击支付、免密支付、先用后付或任何扣款控件;订单状态需要人核查。'); return; }
|
||||
showDialog('只核查同一提交记录', '围栏后只能围绕 SUB-DEMO-0042 调和。这里没有重新领取、放弃授权、重新申请围栏或再次提交的动作。');
|
||||
});
|
||||
$('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(); });
|
||||
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(); });
|
||||
window.addEventListener('resize', hideRecordMenu);
|
||||
$('export-log').addEventListener('click', exportDemo);
|
||||
$('close-demo').addEventListener('click', () => {
|
||||
const unsafe = ['trial','dry_run','order_prefence','fencing','fenced'].includes($('scenario').value);
|
||||
@@ -527,7 +803,7 @@
|
||||
});
|
||||
$('dialog-close').addEventListener('click', () => $('info-dialog').close());
|
||||
$('info-dialog').addEventListener('click', (e) => { if (e.target === $('info-dialog')) $('info-dialog').close(); });
|
||||
render('idle', false); appendLog('状态', '原型已加载;未连接任何服务或设备。');
|
||||
renderRecords(); render('idle', false); appendLog('状态', '原型已加载;未连接任何服务或设备。');
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
+20
-10
@@ -85,23 +85,29 @@
|
||||
|
||||
## 三、desk 端界面结构
|
||||
|
||||
桌面端不是网页,用固定两页签,不做多级导航:
|
||||
桌面端产品名为“采购工具”,不是网页;使用顶部固定两页签,不做多级导航,默认打开采购执行:
|
||||
|
||||
| 页签 | 职责 | 用户故事 | 交互 |
|
||||
| --- | --- | --- | --- |
|
||||
| 采购执行 | 连接状态、轮询开关、当前任务与步骤、待人工提示 | US-003、US-008 | IX-007、IX-008 |
|
||||
| 设备与参数 | 设备档案、ADB 路径与 serial、超时参数、连接检查 | US-003 | IX-007 |
|
||||
| 采购执行 | 连接与会话状态、当前任务与可选图片、滚动日志、执行记录 | US-003、US-008 | IX-007、IX-008 |
|
||||
| 配置 | 设备档案、ADB 路径与 serial、常用超时参数、连接检查 | US-003 | IX-007 |
|
||||
|
||||
### 采购执行页
|
||||
|
||||
- 顶部:web 端连接状态 + 设备连接状态,两者都绿才能启动轮询。
|
||||
- 顶部同时显示拼多多 App 实际版本与已取证版本;不一致时停止轮询并提示重新取证。
|
||||
- 中部:当前任务卡(商品、规格、数量、**当前是第一趟还是第二趟**、当前步骤、剩余租约)。
|
||||
- 底部:轮询控制条(开始轮询 / 停止轮询)+ 下次轮询倒计时 + 连续失败计数。
|
||||
- 顶部状态区:web、设备、拼多多 App 实际 / 已取证版本和会话状态;开始 / 停止轮询固定在
|
||||
最右侧且只控制轮询会话。版本不一致时 fail closed 并提示重新取证。
|
||||
- 宽屏三块工作区:左上当前任务,左下滚动日志,右侧执行记录;窄屏按此顺序改为单列。
|
||||
- 当前任务左侧显示商品、规格、数量、**当前是第一趟还是第二趟**、步骤和剩余租约;右侧约
|
||||
1/3 显示可选商品 / 证据预览。无可信图片时保持空态,有图必须标来源与采集时间。
|
||||
- 滚动日志标题区同时显示下次轮询、连续失败和本次完成,不再使用独立会话卡或底部控制条。
|
||||
- 执行记录只显示标题、状态两列,任务编号与时间放在标题次行,按采集时间倒序。单击选择;
|
||||
双击、Enter、可见“查看所选记录”或右键打开详情。
|
||||
- 记录详情上部左侧显示原始文字、右侧显示图片证据,下部显示结构化采购结果;关闭后焦点
|
||||
返回原行。图片缺失不使用其他来源凑合。
|
||||
- **待人工时整页显著变色并说明缺什么**,不要让执行员盯着一个静止画面猜。
|
||||
- 关闭窗口即停止轮询;连续失败达阈值自动停止并显示原因。
|
||||
|
||||
### 设备与参数页
|
||||
### 配置页
|
||||
|
||||
- 设备档案必须**显式填写 serial**,不允许留空自动选——同一手机 USB + WiFi 同时在线时
|
||||
自动选会失败(见[架构设计](04-architecture.md)第六节)。
|
||||
@@ -131,8 +137,12 @@
|
||||
| `SpecTrialCard` | 任务详情 | 试选确认卡(MVP 签名组件) |
|
||||
| `AuthorizePanel` | 任务详情 | 确认 / 退回 / 放弃授权 |
|
||||
| `PaymentCheckCard` | 任务详情 | 待付款核对与标记完成 |
|
||||
| `DeviceStatusBar` | desk 采购执行页 | 双连接状态 |
|
||||
| `PollControls` | desk 采购执行页 | 轮询开关、倒计时、失败计数 |
|
||||
| `DeviceStatusBar` | desk 采购执行页 | web、设备与 App 版本三项就绪状态 |
|
||||
| `PollControls` | desk 采购执行页 | 顶部固定的轮询开关与会话状态;日志标题区承载倒计时、失败和完成计数 |
|
||||
| `CurrentTaskPanel` | desk 采购执行页 | 当前任务文字、步骤与带来源的可选图片证据 |
|
||||
| `ExecutionLog` | desk 采购执行页 | 最新在上的滚动日志与假诊断导出 |
|
||||
| `ExecutionRecordTable` | desk 采购执行页 | 标题 / 状态两列、时间倒序和稳定记录 ID |
|
||||
| `ExecutionRecordDialog` | desk 采购执行页 | 原始文字、图片证据、采购结果与焦点恢复 |
|
||||
|
||||
## 六、原型
|
||||
|
||||
|
||||
+59
-2
@@ -87,8 +87,8 @@ desk 端将使用 PySide6 / Qt Widgets,但当前没有可确认的桌面应用
|
||||
危险、黄色不确定、绿色就绪。图标使用内联 SVG 或文字,不用 emoji。
|
||||
- 导航只保留两个固定页签;同一任务区只有一个主动作。开始 / 停止轮询的位置稳定,
|
||||
禁用时在控件附近说明原因。
|
||||
- 适配 compact / medium / wide:窄宽度改为单列,宽屏把当前任务与事件日志并排;不依赖
|
||||
hover 才能看到关键信息。
|
||||
- 适配 compact / medium / wide:窄宽度按当前任务、滚动日志、执行记录改为单列,宽屏显示
|
||||
左侧上下两块与右侧跨行记录表格;不依赖 hover 才能看到关键信息。
|
||||
- 支持键盘遍历、可见焦点、`aria-live`、高对比媒体查询、深浅色 token 与
|
||||
`prefers-reduced-motion`。原型可以提供主题 / 状态切换,但必须标注为演示工具。
|
||||
- 每个文件 CSS / JS 全内联、可复位假数据、零网络请求;不得调用 ADB、shell、API 或打开 App。
|
||||
@@ -140,6 +140,63 @@ desk 端将使用 PySide6 / Qt Widgets,但当前没有可确认的桌面应用
|
||||
|
||||
## 执行记录
|
||||
|
||||
### 2026-08-03 · 第二轮采购工具工作区实现
|
||||
|
||||
- 任务继续保持 `DOING`:第二轮自动验证已完成,但 `needs_human_review: true`,仍需人工确认
|
||||
三块布局、图片证据密度、执行记录表格和详情弹窗。
|
||||
- 两页应用壳统一为“采购工具”,固定页签改为“采购执行 / 配置”;HTML 文件名保持不变,避免
|
||||
无业务价值的路径迁移。采购执行仍为默认入口。
|
||||
- 删除底部轮询控制整行,把开始 / 停止轮询固定到顶部状态区右侧。按钮只控制会话:真机步骤、
|
||||
围栏申请、围栏后和结果不明确时按安全规则禁用;核查说明、诊断导出和前往配置改放状态
|
||||
Banner 的对象级动作区,不再借用会话按钮。
|
||||
- 宽屏工作区为左约 65% / 右约 35%:左上当前任务使用文字约 2/3、预览约 1/3;默认空态,
|
||||
有任务时只显示带来源和采集时间的内联假证据。左下改为滚动日志,并把倒计时、连续失败和
|
||||
本次完成压缩到标题区。右侧执行记录跨两块高度。
|
||||
- 执行记录以稳定记录 ID 管理当前行,按 ISO 采集时间倒序;可见列只有标题和状态,任务编号
|
||||
与时间放在标题次行。单击只选择,双击、Enter、可见按钮和右键菜单共用同一打开详情命令。
|
||||
- 详情 `dialog` 上部左侧显示原始文字和时间线、右侧显示图片证据,下部显示结构化采购结果;
|
||||
缺图时保持明确空态。关闭按钮、Esc 和窗口关闭后焦点回到原表格行。任何记录都没有提交订单、
|
||||
重试提交或付款控件。
|
||||
- `windows-ui-ux` 使本轮明确了静态 Tab 身份、会话 / 对象命令分区、当前行与焦点分离、指针与
|
||||
键盘等价入口、模态焦点恢复、紧凑宽度重排以及 HTML 不能替代 Qt 原生验收的边界。
|
||||
|
||||
**第二轮 HTML → PySide6 / Qt Widgets 交接增量**
|
||||
|
||||
| 原型项 | 核心交互契约 | 推荐 Qt Widgets 映射 | 状态 / 线程要求 |
|
||||
| --- | --- | --- | --- |
|
||||
| “采购执行 / 配置”固定页签 | 稳定 page id、默认采购执行、切换保留状态 | 不可关闭 `QTabWidget`;页面对象常驻或按明确策略缓存 | `currentChanged` 只切页面,不重置模型或运行任务 |
|
||||
| 顶部轮询控制 | 只控制普通轮询会话,位置稳定;禁用就近说明原因 | 共享 `QAction` + `QPushButton` + 状态 `QLabel` | enabled / label / reason 由会话状态机单向派生 |
|
||||
| 当前任务与商品预览 | 文字 2/3、图片 1/3;无可信图片保持空态,有图标来源与时间 | `QGridLayout` / `QSplitter` + 只读字段 + `QLabel` / `QPixmap` | 图片下载、解码和裁剪离开 GUI 线程;结果带任务 / 请求 ID,过期回调丢弃 |
|
||||
| 滚动日志与指标 | 最新在上;日志脱敏;高频事件不阻塞 UI | `QTableView` + `QAbstractTableModel`,指标使用只读 `QLabel` | 批量 / 节流更新,模型只在 GUI 线程发通知 |
|
||||
| 执行记录表格 | 仅标题 / 状态;时间倒序;稳定 ID 保留当前行 | `QTableView` + `QAbstractTableModel` + `QSortFilterProxyModel` | `clicked` 只选中,`doubleClicked` / `activated` 共用详情 `QAction`;不依赖视图行号 |
|
||||
| 记录详情 | 原始文字 / 图片 / 采购结果;Esc 与关闭统一;恢复原行焦点 | 有所有者的 `QDialog` + 布局容器 + `QDialogButtonBox` | 打开时传记录 ID 快照;关闭后按代理模型映射回稳定 ID,记录已删除则聚焦表格 |
|
||||
|
||||
PySide6 / Qt 的精确版本仍需 T-002 初始化后核对;这里不承诺特定材质、主题 API 或原生表格
|
||||
性能,也不把 HTML 的焦点 / 对话框行为当作 Narrator、UI Automation 或 DPI 验收证据。
|
||||
|
||||
**第二轮已运行验证**
|
||||
|
||||
- Node 内联静态检查:两个 `desk-*.html` 的 doctype、viewport、原型横幅、减少动画、唯一 id、
|
||||
内联脚本语法、相对页签链接和零外部资源均通过;执行记录表头严格为“标题 / 状态”,页面无
|
||||
`poll-bar`,按钮危险面未发现提交订单、重试提交、继续付款、免密支付或先用后付。
|
||||
- Playwright 逐项切换采购执行页 18 个状态:轮询命令保持会话语义;真机、围栏和不明确状态
|
||||
禁用原因正确;核查 / 导出 / 配置作为独立上下文动作;默认无图,有任务时显示内联假证据。
|
||||
- 表格交互:四条假记录按 13:52、12:24、10:06、09:18 倒序;双击与 Enter 打开对应记录,
|
||||
右键先选中命中行再显示查看菜单;详情三分区正确,缺图记录显示空态,Esc 关闭并恢复原行。
|
||||
- 采购执行 / 配置两页在 375 / 768 / 1024 / 1440 px 共 8 个组合无页面级水平溢出;执行详情
|
||||
在 1440 px 无内部滚动,在 375 px 单列且无水平溢出。浏览器累计 0 error / 0 warning,网络
|
||||
只有当前本地 HTML 文档。
|
||||
- `python scripts/validate_agent_context.py` 与 `git diff --check`:提交前运行并记录最终结果。
|
||||
|
||||
**未验证与待人工确认**
|
||||
|
||||
- 没有生产 PySide6、Model/View、图片采集、线程、ADB 或 web 代码,因此不证明真实图片能从
|
||||
拼多多取得,也未运行桌面端单元测试、编译或打包。
|
||||
- 待人确认:顶部状态区在常用窗口下的信息密度;当前任务 2/3 + 1/3、整体 65% + 35% 比例;
|
||||
两列执行记录是否够用;详情 940 px 宽屏尺寸及窄屏滚动;“执行记录”术语是否沿用。
|
||||
- 原生实现仍需验证 Windows 100%–200% DPI、多显示器、贴靠、浅 / 深 / 高对比、中文输入法、
|
||||
Narrator / Inspect、表格虚拟化、线程与最终打包产物。本轮没有真机验收。
|
||||
|
||||
### 2026-08-03 · 原型待人工确认
|
||||
|
||||
状态保持 `DOING`。本轮只完成 Phase 0 浏览器原型,不连接 web、ADB 或拼多多,也不把浏览器
|
||||
|
||||
Reference in New Issue
Block a user