diff --git a/AGENTS.md b/AGENTS.md index bd6eeda..bd970a5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,8 @@ cmbuyer 是一个自动化采购系统:**采购服务**(网页端,`admin/` **系统只创建待付款订单,任何情况下都不自动付款。** -当前状态:仓库只有文档,尚未开始编码。阶段为 Phase 0。 +当前状态:两端骨架、基础模型、登录和真机取证脚手架已落地;Phase 1 真机取证与不依赖页面判据的 +Phase 2 服务端任务并行。实时快照见 [`docs/current-state.md`](docs/current-state.md)。 ## 必读顺序 @@ -33,7 +34,9 @@ cmbuyer 是一个自动化采购系统:**采购服务**(网页端,`admin/` 系统**会**点击「提交订单」创建待付款订单,但必须满足四个前置条件(授权未消费且服务端 提交围栏已建立、闸门二通过、闸门三通过、控件唯一),且**只点一次**、点击后无论结果 都不重试。围栏申请失败或响应不明时不得点击;围栏建立后只能调和同一提交记录。 -**第一趟试选的代码路径不得引用任何下单函数**,必须有测试证明不可达。 +管理员点击“开始采购(只创建待付款订单)”是唯一的人类授权动作。T-103 的规格选择/读价隔离 +验证路径不得引用数量、确认页、提交或付款函数;后续能力必须按真机取证任务逐段开放,并有静态 +调用链测试证明未获准能力不可达。 ### 2. 安全边界只能收紧 @@ -141,8 +144,8 @@ python scripts/validate_agent_context.py **跨端契约改动必跑完整门禁**——两端会同时坏。 -代码尚未初始化,上述命令在 T-001 / T-002 完成前不可运行;届时由对应任务替换为真实命令 -并同步文档。 +两端已初始化;跨端契约改动还需在仓库根运行 `./init.ps1`(或 `./init.sh`)完成安装、测试、 +vet/build、compileall 和上下文门禁。 ## 风格 diff --git a/README.md b/README.md index 63f287b..78c3678 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,14 @@ ## 它做什么 一笔外部订单进来,采购人员需要去拼多多找到同款、选对颜色尺码、下单、把订单号抄回系统。 -cmbuyer 把这个过程自动化,人只在两个点介入:**机器选对了吗**和**付不付款**。 +cmbuyer 把这个过程自动化:管理员明确“买什么、买多少、最多多少钱”并点击开始采购,系统只 +创建待付款订单,**是否付款始终由人决定**。 ```text 手工填链接(MVP) / Excel · ERP(V2) │ v - 采购服务(admin/,Go) 建单 · 试选确认 · 下单授权 · 审计 + 采购服务(admin/,Go) 建单 · 开始采购授权 · 围栏 · 审计 │ HTTP v 采购工具(client/,Python) 领任务 · 跑流程 · 回传 @@ -23,31 +24,22 @@ cmbuyer 把这个过程自动化,人只在两个点介入:**机器选对了 Android 手机(拼多多 App) ``` -## 两趟执行 +## 单趟执行 -MVP 只做**任务自带商品链接**的情形,分两趟跑完: +MVP 只做**任务自带商品链接**的情形:管理员先把任务保存为 `DRAFT`,再在表格中勾选并点击 +“开始采购(只创建待付款订单)”。该点击创建一次性授权,锁定商品、颜色、尺码、数量和最高总价。 -| 趟次 | 做什么 | -| --- | --- | -| **第一趟 · 试选** | 开商品 → 精确勾选颜色分类和尺码 → 读单价 → 截图 → **退出释放手机** → 回传 | -| **人工确认** | 人在网页端看「机器选对了吗」→ 确认并**锁定单价** | -| **第二趟 · 下单** | 重新开商品 → 重新选同一规格 → **三道价格闸门** → 提交订单一次 → 转「待付款」 | - -为什么分两趟:一台手机是瓶颈,不能停在规格面板上等人。代价是走两遍,换来手机不空闲, -且第二趟能抓住价格变动。 - -**三道价格闸门**:① 第一趟规格面板读价 ② 第二趟重读必须与授权价一致 -③ 订单确认页「实付款」不超上限。任一道读不到或不通过即停,转人工。 +采购工具领取后在同一次设备会话中完成:打开商品 → 精确选择规格 → 闸门一读 SKU 单价并校验 +上限 → 设置并复核数量 → 闸门二重读规格与同价 → 进入确认页 → 闸门三校验应付总额 → 服务端 +原子建立提交围栏 → 精确点击一次“提交订单” → 转待付款。中间不再回网页端等“机器选对了吗”。 价格**只在规格面板和订单确认页读**——别处的价格文本被拆成多个节点、带券后前缀、 实付价与原价混在一起,不可靠。 ## 现在处于什么阶段 -**Phase 0 · 地基。仓库目前只有文档,尚未开始编码。** - -下一步:T-005(网页端 MVP 原型)与 T-006(桌面端 MVP 原型)的文件和自动检查已完成, -请先人工确认 6 个 HTML 原型,再进入 T-001 / T-002 骨架和生产实现。 +**Phase 1 真机取证与 Phase 2 的安全服务端工作并行。** 两端骨架、基础模型、登录和真机基线已 +落地;T-111 正在冻结单趟采购契约,T-103 随后继续规格精确选择与读价,T-202 等待主审合入。 详见 [`docs/current-state.md`](docs/current-state.md)。 **生死线是 M2**:真机能按链接打开商品、精确勾选颜色分类和尺码、**读到该 SKU 单价**。 @@ -74,7 +66,7 @@ MVP 只做**任务自带商品链接**的情形,分两趟跑完: 4. 规格按维度精确匹配,防前缀碰撞,找不到即停 5. 数量设置后必须读回复核 6. **三道价格闸门**,任一道读不到或不通过即停 -7. **第一趟绝不下单**——试选路径不得引用下单函数 +7. **能力分层**——T-103 规格验证路径不得引用数量、确认页或下单函数;后续能力逐段取证 8. 检测到外部支付交接立即停止,不读取不保存凭据 9. 检测到验证码 / 风控 / 人脸 / 短信校验立即停止,不绕过 10. 只读非敏感摘要,不提取收货地址原文、手机号、支付凭据 diff --git a/docs/00-ai-start-here.md b/docs/00-ai-start-here.md index 1a4865f..8070a6f 100644 --- a/docs/00-ai-start-here.md +++ b/docs/00-ai-start-here.md @@ -9,13 +9,10 @@ cmbuyer 是一个自动化采购系统:**采购服务**(网页端,`admin/` **采购工具**(桌面端,`client/`)驱动 Android 手机在拼多多完成找货和下单, **付款始终由人完成**。 -第一版 MVP 只做**任务自带商品链接**的情形,**分两趟跑**: - -1. **第一趟试选**:桌面端定时领取 → 开商品 → 精确勾选颜色分类和尺码 → 读单价 → - 截图 → **退出释放手机** → 回传。 -2. **人工确认**:管理员在网页端看「机器选对了吗」,确认后签发授权并**锁定单价**。 -3. **第二趟下单**:重新开商品 → 重新选同一规格 → **三道价格闸门** → 提交订单一次 → - 任务转「待付款」,人在拼多多核对后付款。 +第一版 MVP 只做**任务自带商品链接**的情形。创建任务不授权;管理员勾选 `DRAFT` 并点击 +“开始采购(只创建待付款订单)”后,服务端锁定商品、规格、数量和最高总价并签发一次性授权。 +采购工具领取后在**同一趟**完成精确选规格、三道价格闸门、数量复核、确认页、提交围栏和唯一一次 +“提交订单”点击。中间不等待人工确认;订单创建后转待付款,由人核对和付款。 ## 上下文读取 @@ -24,7 +21,7 @@ cmbuyer 是一个自动化采购系统:**采购服务**(网页端,`admin/` 1. [`01-vision.md`](01-vision.md):为什么做、为谁做、什么不做。 2. [`02-requirements.md`](02-requirements.md):MVP 要什么、怎么算达成。 3. [`03-tech-stack.md`](03-tech-stack.md):两端各自的技术选型与验证矩阵。 -4. [`04-architecture.md`](04-architecture.md):双端职责、**两趟执行**、**三道价格闸门**、安全边界、数据模型。 +4. [`04-architecture.md`](04-architecture.md):双端职责、**单趟执行**、**三道价格闸门**、安全边界、数据模型。 5. [`05-coding-rules.md`](05-coding-rules.md):写代码前必须遵守的规则。 6. [`06-tasks.md`](06-tasks.md):阶段路线图与建议拆分。 7. [`tasks/README.md`](tasks/README.md):任务文件约定。 @@ -61,7 +58,7 @@ cmbuyer 是一个自动化采购系统:**采购服务**(网页端,`admin/` ### 2. 安全边界只能收紧,不能放宽 [`04-architecture.md`](04-architecture.md) 第四节的硬约束(不付款、提交订单四条件与服务端围栏、 -规格精确匹配、提交控件唯一、数量必须复核、三道价格闸门、第一趟不下单、外部支付停止、 +规格精确匹配、提交控件唯一、数量必须复核、三道价格闸门、隔离能力不越界、外部支付停止、 安全校验停止、敏感信息不提取、授权一次性)**不得在任务中顺手放宽**。 确需变更时先改架构文档并说明理由,再动代码。任何「为了让流程跑通先放宽一下」的改动 @@ -77,11 +74,11 @@ cmbuyer 是一个自动化采购系统:**采购服务**(网页端,`admin/` 理由与三道闸门的定义见 [`04-architecture.md`](04-architecture.md) 第三节。 -### 4. 第一趟绝不下单 +### 4. 真机能力逐段取证、隔离开放 -试选阶段只勾选规格和读价,**绝不点击「现在买」或任何进入下单流程的入口**。 -第一趟的代码路径不得引用 `go_to_order_confirm()` 与 `submit_order()`,必须有测试 -证明它们不可达。 +T-103 的 `SkuSelectionFlow` 只允许证据绑定的受控入口、精确选择、读价和安全退出;不得引用 +数量、确认页、提交围栏、提交或付款能力。T-105~T-107 分别取证后才可组合。生产单趟只有在全部 +能力均获证据、三道闸门通过且服务端围栏明确许可后才能点击一次;支付能力永不开放。 ## 当前阶段 @@ -90,19 +87,18 @@ cmbuyer 是一个自动化采购系统:**采购服务**(网页端,`admin/` 执行按任务依赖驱动,**不按 Phase 整段串行等待**。当前优先路径: -1. T-001~T-004 与 T-101~T-102 已完成,继续推进 **T-103 真机取证**。 -2. T-103 进行时并行推进 T-201 管理会话 → T-202 `DRAFT` 建单与基础列表;两者不得启动试选, - 也不得引入机器结果、规格面板单价或证据字段。 -3. T-103 结论确认后,再推进 T-203 批量开始试选、T-204 试选证据详情、T-205~T-207, - 并按依赖推进 T-104 → T-107 后续真机安全判据。 -4. Phase 3:双端打通与**第一趟试选**端到端。 -5. Phase 4:**第二趟下单**与收尾。 +1. T-111 先冻结“开始采购授权”的单趟契约;同时主审并合入只创建 `DRAFT` 的 T-202。 +2. T-111 完成后恢复 **T-103 真机取证**;按依赖推进 T-104 → T-107 的分段安全判据。 +3. admin agent 先做 T-209 把旧两趟 schema / 状态机迁到单趟,再推进 T-203 服务端批量开始采购 / + 一次性授权;两者不依赖页面选择器。T-205 起的 attempt 与证据字段等待 T-103。 +4. Phase 3:双端打通与单趟围栏前 dry-run。 +5. Phase 4:单趟真实提交、待付款、调和与收尾。 6. V2 及以后:图搜、Excel、ERP、订单自动核对、AI 辅助。 > **M2 是本项目的生死线**:真机能按链接打开商品、精确勾选颜色分类和尺码、 > **读到该 SKU 的单价**(T-103)。前序项目正是卡在选规格和读价。 -> M2 不通过之前不要写**依赖真机可读字段或会启动试选**的 Phase 2 功能;T-201 与仅创建 -> `DRAFT` 的 T-202 可以并行。原型只确认流程与信息架构,实际可读字段仍以真机证据为准。 +> M2 不通过之前不要写**依赖真机可读字段的生产执行逻辑**;管理会话、`DRAFT` 建单和只锁定 +> 已有任务字段的服务端授权事务可以并行。实际可读字段仍以真机证据为准。 ## 领取任务规则 @@ -123,12 +119,11 @@ cmbuyer 是一个自动化采购系统:**采购服务**(网页端,`admin/` 只做: -- **手工填链接**建单、任务查询、勾选待开始任务后批量开始第一趟试选 -- 桌面端**定时轮询**领取(试选与下单两类) -- 第一趟试选:精确勾选规格、读单价、截图、退出释放手机 -- 人工确认「机器选对了吗」并签发授权(**锁定单价**) -- 授权超时与放弃,任务不会被永久锁死 -- 第二趟下单:三道闸门全过后提交订单一次 +- **手工填链接**建单、任务查询、勾选待开始任务后批量“开始采购” +- 点击开始采购即签发锁定商品、规格、数量和最高总价的一次性授权 +- 桌面端定时轮询只领取已授权任务,在同一趟精确选规格、两次读价、复核数量和确认页金额 +- 围栏前失败/过期可人工回到待开始;围栏后只能调和 +- 三道闸门全过、服务端围栏明确许可后提交订单一次 - 真实点击前服务端原子建立提交围栏;围栏失败不点击,围栏后只调和同一提交记录 - 待付款展示订单截图,人核对付款后手工标记完成 - 失败分类与转人工 @@ -168,7 +163,7 @@ cmbuyer 是一个自动化采购系统:**采购服务**(网页端,`admin/` | 采购服务页面 | `02-requirements.md` 验收 → `07-user-stories.md` → `08-interaction-checklist.md` → `routes.md` → `04-architecture.md` | | 采购工具界面 | 同上,`routes.md` 看第三节桌面端结构 | | 设备侧 API | `api.md` → `04-architecture.md` 数据模型与鉴权边界 | -| 真机自动化 | `04-architecture.md` 第三节两趟执行与三道闸门 → 第四节边界 → `api.md` 第三节模块合约 → **先真机取证** | +| 真机自动化 | `04-architecture.md` 第三节单趟执行与三道闸门 → 第四节边界 → `api.md` 第三节模块合约 → **先真机取证** | | 数据模型 | `04-architecture.md` 第五节;schema 变化必须同步 `api.md` 和 `current-state.md` | | 部署 / 运行 | `03-tech-stack.md` → `current-state.md` | diff --git a/docs/01-vision.md b/docs/01-vision.md index ef78d90..17d7cd6 100644 --- a/docs/01-vision.md +++ b/docs/01-vision.md @@ -5,15 +5,15 @@ cmbuyer 要解决:**采购人员为了履约一笔外部订单,必须手工去拼多多找到同款商品、选对 颜色尺码、下单,再把订单号抄回系统——这个过程重复、易错、且无法追溯。** -> 让采购人员把「买什么」一次说清楚,由系统驱动手机完成找货、选规格和下单,人只在 -> 两个关键点介入:**挑哪一个**和**付不付款**。 +> 让采购人员把“买什么、买多少、最多多少钱”一次说清楚,并明确点击开始采购;系统驱动手机 +> 精确选规格并创建待付款订单,**付不付款始终由人决定**。 它不是无人值守的抢购脚本,也不是绕过平台规则的爬虫。它是一个**带人工闸门的采购执行 工具**:机器负责重复劳动,人保留花钱的决定权。 ## 二、目标用户 -- **采购管理员**:在网页端建单、复核候选商品、签发下单授权、查看执行证据。 +- **采购管理员**:在网页端建单、点击开始采购签发一次性授权、查看执行证据。 - **采购执行员**:在桌面端连接手机、启动批次、处理需要人工接管的任务、完成付款。 - **ERP 对接身份**:只读同步第三方系统的货运单与商品明细,不参与采购决策。 - **系统管理员(后续)**:人员、设备、权限和审计策略管理,MVP 不提供完整界面。 @@ -27,7 +27,8 @@ cmbuyer 要解决:**采购人员为了履约一笔外部订单,必须手工 绝不猜测。宁可少做,不可做错。 - **先走确定路径**:MVP 只做已知商品链接的情形。图片搜索推到 V2,且届时其唯一职责 是产出 `goods_id`,不在搜索结果页上做价格或规格判断。 -- **人站在不可逆动作正前方**:机器先试选并回传,人看过之后才授权下单。 +- **授权点明确且前置**:创建任务不授权;管理员点击“开始采购”才允许创建一笔待付款订单。 +- **实时闸门不依赖旧截图**:授权后同一设备会话两次读取规格面板价格,并在确认页校验总额。 - **价格只在可靠位置读**:规格面板和订单确认页。别处的数字一律不信。 - **失败要可诊断**:不能只返回「失败」,必须有步骤、错误码、截图和页面快照。 - **证据分层**:人工决策需要的证据上传服务端,完整执行轨迹留在桌面端本地。 @@ -37,9 +38,9 @@ cmbuyer 要解决:**采购人员为了履约一笔外部订单,必须手工 | 价值点 | 用户得到什么 | | --- | --- | | 消除重复劳动 | 不再逐条手工搜索、选规格、抄订单号 | -| 决策集中可控 | 所有「买哪个」的决定收敛到网页端一处,有据可查 | +| 决策集中可控 | 商品、规格、数量、最高总价和开始采购授权收敛到网页端,有据可查 | | 资金边界清晰 | 系统能下单不能付款,误操作不会直接造成损失 | -| 执行可追溯 | 每笔采购留下候选、截图、授权理由和订单核对记录 | +| 执行可追溯 | 每笔采购留下授权、三道闸门、截图、围栏和订单核对记录 | | 批量顺序执行 | 一次导入多条,按顺序跑,遇到问题停在该停的地方 | ## 五、不做什么(非目标) diff --git a/docs/02-requirements.md b/docs/02-requirements.md index 332672d..c3a685b 100644 --- a/docs/02-requirements.md +++ b/docs/02-requirements.md @@ -1,50 +1,48 @@ # 需求 -> 本文只描述**要什么**与**怎么算达成**,用产品 / 用户语言表达,**不涉及技术实现**。 -> 技术方案、数据结构、字段定义见 [架构设计](04-architecture.md)。 +> 本文只描述**要什么**与**怎么算达成**,用产品 / 用户语言表达。技术方案、数据结构与 +> 字段定义见[架构设计](04-architecture.md)。 ## 一、业务现状 | 项 | 状态 | | --- | --- | | 用户 | 采购人员为履约外部订单,逐条手工在拼多多找同款、选颜色尺码、下单、抄回订单号 | -| 任务来源 | 第三方 ERP(顺运宝)货运单与商品明细为主;Excel 批量导入和手工填链接为补充 | -| 数据 | ERP 提供商品标题、规格、数量、参考图;部分明细带拼多多链接,部分只有图 | -| 现有系统 | 两个前序原型(见 [愿景](01-vision.md) 第六节),代码不直接复用 | +| MVP 任务来源 | 管理员手工填写拼多多商品链接、规格、数量和金额上限 | +| 后续任务来源 | Excel 与第三方 ERP(顺运宝)货运明细 | | 设备 | 一台 Windows 电脑 + 一台已登录拼多多的 Android 手机,USB 或 WiFi ADB 连接 | -| 约束 | 不得绕过平台风控;付款必须人工;手机需保持亮屏解锁 | +| 约束 | 不绕过平台风控;系统只创建待付款订单;付款必须由人完成;手机保持亮屏解锁 | ## 二、用户角色 -- **采购管理员**:在网页端建单、查询、复核候选、签发下单授权、查看审计记录。 -- **采购执行员**:在桌面端连接设备、启动批次、处理待人工任务、在拼多多完成付款。 +- **采购管理员**:在采购服务建单、查询、点击“开始采购”签发一次性采购授权、查看审计记录。 +- **采购执行员**:在采购工具连接设备、启动轮询、处理待人工任务、在拼多多完成人工付款。 - **ERP 对接身份**:只读同步货运单与商品明细,不能建单、授权或访问采购结果。 -- **设备身份**:一台已授权桌面端实例,用于领取任务和回传结果,不能建单或授权。 +- **设备身份**:一台已授权采购工具实例,用于领取已授权任务和回传结果,不能建单或授权。 - **未登录用户**:不能访问任何任务、图片、证据或设备接口。 ## 三、功能清单 ### 第一版 MVP(最小闭环) -MVP 只做**任务自带商品链接**的情形,分两趟执行:第一趟试选并回传,人确认后第二趟下单。 -流程见[架构设计](04-architecture.md)第三节。 +MVP 只做**任务自带商品链接**的情形。管理员点击“开始采购”后,采购工具在同一次设备会话中 +完成选规格、校验价格与数量、创建待付款订单;中间不再回到采购服务等待“机器选对了吗”。 | ID | 功能 | 用户能做什么 | 优先级 | 关联用户故事 | | --- | --- | --- | --- | --- | -| F-001 | 手工建单 | 管理员在任务列表内填写任务名称、拼多多链接、颜色分类、尺码、数量、价格上限,保存一条待开始任务 | P0 | US-001 | -| F-004 | 任务查询 | 管理员按关键词、状态、时间范围找到目标任务 | P0 | US-002 | -| F-005 | 桌面端定时领取 | 执行员启动会话后,桌面端定时轮询领取待试选和已授权两类任务 | P0 | US-003 | -| F-006 | 第一趟试选 | 系统打开商品、按维度精确勾选颜色分类和尺码、读单价、截图后退出 | P0 | US-003 | -| F-007 | 试选结果回传 | 管理员看到机器实际选中的规格、单价、合计和规格面板截图 | P0 | US-004 | -| F-008 | 人工确认与授权 | 管理员确认机器选对了,签发一次性授权并锁定价格;或退回不买 | P0 | US-004、US-005 | -| F-009 | 第二趟下单 | 系统重新选同一规格、过三道价格闸门后提交订单,回传订单截图 | P0 | US-005 | -| F-010 | 授权超时与放弃 | 管理员在授权卡住时能放弃并重新确认,任务不会被永久锁死 | P0 | US-005 | -| F-011 | 结果与失败分类回传 | 管理员看到成功、失败、已取消、待人工,以及可区分的失败原因和证据 | P0 | US-002、US-008 | -| F-013 | 登录与身份隔离 | 管理员用账号登录网页端;桌面端用设备凭据接入,两者权限不互通 | P0 | US-007 | -| F-017 | 下单 dry-run、提交围栏与调和 | 真机先只读演练到订单确认页;真实点击前由服务端原子冻结授权;点击后只调和结果、绝不重试 | P0 | US-005、US-008 | -| F-018 | 批量开始试选 | 管理员勾选一条或多条待开始任务,统一放入第一趟试选队列 | P0 | US-010 | +| F-001 | 手工建单 | 填写标题、拼多多链接、颜色分类、尺码、数量、最高总价,保存为待开始任务 | P0 | US-001 | +| F-004 | 任务查询 | 按标题关键词、状态、时间范围找到任务,双击行查看详情 | P0 | US-002 | +| F-005 | 桌面端定时领取 | 执行员启动会话后定时领取已获采购授权的任务 | P0 | US-003 | +| F-006 | 单趟采购执行 | 打开商品、精确选择规格、校验价格、设置数量、进入确认页并创建待付款订单 | P0 | US-003、US-005 | +| F-007 | 执行证据回传 | 管理员查看实际规格、三道闸门、原始页面截图和步骤记录 | P0 | US-002、US-008 | +| F-008 | 开始采购授权 | 管理员批量点击一次,原子锁定每条任务并签发一次性待付款订单授权 | P0 | US-005、US-010 | +| F-010 | 围栏前过期与重新授权 | 未建立提交围栏的失败或过期任务回到待开始,可由管理员重新点击开始采购 | P0 | US-005 | +| F-011 | 结果与失败分类回传 | 查看成功、失败、已取消、待人工及可诊断原因和证据 | P0 | US-002、US-008 | +| F-013 | 登录与身份隔离 | 管理员用账号登录;采购工具用设备凭据接入,权限不互通 | P0 | US-007 | +| F-017 | 提交围栏与调和 | 真机点击前服务端原子冻结授权;点击后只调和同一提交,绝不重试 | P0 | US-005、US-008 | +| F-018 | 批量开始采购 | 勾选一条或多条待开始任务,统一授权采购工具创建待付款订单 | P0 | US-010 | -> F-002、F-003、F-012 已移出 MVP,编号保留不重用,见下表。 +> 原 F-009 的“两趟下单”并入 F-006;F-002、F-003、F-012 移出 MVP,编号不重用。 ### 后续迭代 @@ -52,153 +50,128 @@ MVP 只做**任务自带商品链接**的情形,分两趟执行:第一趟试 | --- | --- | --- | | F-002 Excel 批量建单 | 上传固定表头表格批量生成任务 | V2,被表头契约待确认阻塞 | | F-003 从 ERP 货运明细建单 | 同步顺运宝货运单后复核生成任务 | V2,被字段映射待确认阻塞 | -| F-012 批量顺序编排与人工接管 | 对已开始任务排序、暂停继续、运行中接管 | V2;MVP 仅支持批量开始试选,实际执行仍由定时轮询领取 | -| F-014 图片搜索路径(B 路径) | 任务只有参考图时搜图产出 goods_id 候选 | V2 | +| F-012 批量编排与运行中接管 | 排序、暂停继续、运行中接管 | V2;MVP 只批量授权,设备逐条领取 | +| F-014 图片搜索路径 | 任务只有参考图时搜图产出 goods_id 候选 | V2 | | F-015 候选对照台 | 多个候选并排对照挑选 | V2,随 F-014 | -| F-016 订单自动核对回读 | 付款后只读读取订单页做五项唯一匹配并回写 | V2;MVP 用截图 + 人眼核对 | +| F-016 订单自动核对回读 | 付款后只读读取订单页做唯一匹配并回写 | V2;MVP 用截图 + 人眼核对 | | F-101 AI 辅助候选判断 | 用模型看搜索结果页截图判断同款 | V2 之后 | | F-102 AI 辅助页面理解 | 规则读不到规格或价格时用模型兜底 | V2 之后 | -| F-103 执行轨迹本地留档 | 记录模型输入输出与规则判断的分歧 | V2 之后 | -| F-104 多设备并行 | 一个桌面端驱动多台手机 | V2 之后 | +| F-103 执行轨迹本地留档 | 记录模型输入输出与规则判断分歧 | V2 之后 | +| F-104 多设备并行 | 一个采购工具驱动多台手机 | V2 之后 | | F-105 完整 RBAC | 管理员、执行员、审核员细粒度权限 | V2 之后 | | F-106 多平台比价 | 淘宝、1688、京东 | V3 | | 支付自动化 | **不在规划内** | 未规划 | ## 四、核心用户故事(MVP) -详细故事以[用户故事清单](07-user-stories.md)为准;本文只维护功能、优先级与 US 编号的 -索引,避免两处成为相互冲突的权威来源。 +详细故事以[用户故事清单](07-user-stories.md)为准。 | 功能 | 用户故事 | 优先级 | | --- | --- | --- | | F-001 | US-001 | P0 | -| F-004、F-011 | US-002 | P0 | +| F-004、F-007、F-011 | US-002 | P0 | | F-005、F-006 | US-003 | P0 | -| F-007、F-008 | US-004 | P0 | -| F-008、F-009、F-010、F-017 | US-005 | P0 | +| F-008、F-010、F-017 | US-005 | P0 | | F-013 | US-007 | P0 | -| F-011 | US-008 | P0 | -| F-018 | US-010 | P0 | +| F-007、F-011、F-017 | US-008 | P0 | +| F-008、F-018 | US-010 | P0 | -US-006(付款前核对)在 MVP 降级为:系统展示订单截图与授权信息,人在拼多多自行核对 -后付款并手工标记完成。自动回读核对是 F-016(V2)。US-009(ERP 建单)随 F-003 移出 MVP。 +US-004 的试选后人工确认已从 MVP 删除。US-006 在 MVP 为:系统展示订单截图与授权信息, +人在拼多多核对、付款并手工标记完成;自动回读核对是 F-016(V2)。 ## 五、验收标准(MVP) -### 建单 +### 建单与授权 -- **F-001**:在任务列表点击创建,填入任务名称、合法拼多多链接、颜色分类、尺码、数量、 - 价格上限后保存,返回任务编号;弹窗关闭,任务按创建时间倒序出现在第一行且状态为待开始。 - 链接格式非法或无法解析出 `goods_id` 时明确报错并保留已填内容。 -- **F-018**:列表只允许勾选待开始任务;全选只覆盖当前筛选结果中可开始的任务。点击 - 「开始试选」后,被选任务原子转为待领取,并明确提示这只是进入第一趟试选队列, - **不签发下单授权、不创建订单、不付款**。任一任务状态已变化时整批不提交,刷新后重选。 +- **F-001**:任务列表点击创建,填入标题、合法拼多多链接、颜色分类、尺码、数量和最高总价后 + 保存,弹窗关闭;新任务按创建时间倒序出现在第一行,状态为 `DRAFT`。非法链接或无法解析 + `goods_id` 时明确报错并保留输入。 +- 创建任务**不构成采购授权**,设备不能领取 `DRAFT`。 +- **F-018**:只允许勾选当前筛选结果中的 `DRAFT`。点击“开始采购(只创建待付款订单)”前, + 界面显示已选数量和最高总额;点击本身即管理员明确授权,不再增加选品确认步骤。 +- 服务端在一个事务中校验整批任务仍为预期版本,逐条锁定 `goods_id`、颜色、尺码、数量、最高 + 总价、授权人和有效期,并把任务转 `PENDING`。任一条冲突时整批不变,刷新后重选。 +- 相同 `start_key` 重放返回同一结果;不得产生第二份有效授权。成功后即时提示“已授权 N 条, + 采购工具只会创建待付款订单,不会付款”。 -### 第一趟:试选 +### 单趟采购执行 -- **F-005**:执行员启动会话后桌面端定时轮询,同时领取待试选与已授权两类任务。 - 两个实例并发领取同一条时只有一个成功,另一个得到明确的「无可领任务」而不是报错。 - **关闭会话即停止轮询;连续失败达到阈值自动停止并提示原因。** -- **F-006**:手机打开对应商品详情页,只点击与当前拼多多版本及本项目真机证据绑定的精确唯一 - **受控规格面板入口**,打开规格面板后按维度精确匹配颜色分类和尺码。当前只确认拼多多 - `8.17.0`、goods_id `937122477375` 上的“快要抢光”;其他入口文案必须分别取证,不能按购买 - 语义泛化。 - **任一维度找不到精确值即停止并转人工,不选相近选项。** 勾选后读取该 SKU 单价 - (闸门一),读不到即转人工,**不用商品详情页正文或搜索页的数字凑合**。 -- **F-006 释放要求**:试选完成后**必须退出商品页释放手机**,不得停在规格面板等待人工。 -- **F-006 硬边界**:第一趟只把上述精确入口点击视为可逆的“打开规格面板”能力;进入面板后 - 不得调整数量、进入订单确认页、点击“提交订单”或任何支付/资金控件,也不得暴露通用任意点击能力。 - 必须有静态调用链和测试证明上述下单语义动作不可达。入口缺失、重复、版本失配或打开后不是已取证 - 面板时立即停止,不尝试“免拼购买 / 单独购买 / 直接拼成”等相似文案。 -- **F-007**:回传商品标题、实际勾选到的颜色分类与尺码、单价、合计(单价 × 数量)和 - **自动脱敏后的**规格面板截图;任务转「等你确认」。原始 screenshot/XML 只能留在采购工具本机 - 隔离目录,不能上传、写日志或提交 Git。 +- **F-005**:采购工具只领取 `PENDING` 且授权有效的任务。并发领取同一条时只有一个成功;关闭 + 会话即停止轮询,连续失败达到阈值自动停止并显示原因。 +- **F-006 受控入口**:只点击与本项目真机证据及 App 版本绑定的精确唯一规格面板入口。当前仅确认 + 拼多多 `8.17.0`、goods_id `937122477375` 的“快要抢光”;其他入口必须分别取证,不按购买语义泛化。 +- **F-006 规格与数量**:颜色分类、尺码必须在各自维度内精确唯一匹配并读回;数量设置后必须读回 + 等于任务值。缺失、重复、禁用、前缀碰撞、读回不一致都停止,不选相近项。 +- **F-006 三道价格闸门**:价格只从规格面板和订单确认页读取,金额使用十进制字符串。 + 1. 选中目标规格后读取当前 SKU 单价,计算 `单价 × 数量`,不得超过授权最高总价。 + 2. 设置并复核数量后再次读取同一规格与单价;规格必须仍相同,单价必须与闸门一完全相等。 + 3. 订单确认页核对规格、数量与应付总额;总额不得超过授权最高总价。 + 任一道读不到、有歧义或不通过都停止并转人工,不从详情页、搜索页或按钮文字凑价格。 +- 同一设备会话连续完成上述步骤;不退出商品再等采购服务确认,也不把旧截图或旧页面读数当作 + 当前提交依据。 -### 决策与资金 +### 提交围栏与资金边界 -- **F-008**:管理员在确认页看到需求与机器所选的对照、单价、合计和截图,点击确认后 - 签发一次性授权,**授权锁定当次试选的单价**。授权重复提交幂等,不产生第二笔订单。 - 管理员也可选择「退回,不买」,任务终止。 -- **F-009 三道闸门**:第二趟重新打开商品并重新勾选同一规格后, - ① 重读单价必须与授权锁定价一致; - ② 数量设置后读回必须精确等于要求值; - ③ 订单确认页「实付款」不得超过授权总额上限。 - **任一道不通过即停止并转人工。** -- **F-009 提交条件**:授权存在且未消费、闸门全过、「提交订单」控件文本精确相等且可 - 点击祖先唯一——四者同时满足,且服务端已原子建立提交围栏,才允许**点击一次**。点击后 - 无论超时、跳外部支付还是遇到安全校验,**一律进入结果调和或人工核查且禁止重试**。 -- **F-010**:授权带过期时间。建立提交围栏前,超时或主动放弃都会作废授权并让任务进入 - 「待重新试选」,不得直接复用旧试选再次确认;建立提交围栏后,授权不得过期或放弃, - 只能调和结果或转人工核查。**任何状态都必须给出安全且可执行的下一步。** -- **F-017 dry-run**:首次真实下单前必须先完成一次只读演练:到订单确认页读取规格、数量、 - 「实付款」,验证提交控件唯一,然后停止并退出;**不点击「提交订单」**。演练结果与证据 - 必须回传,真实提交不得把旧演练当成当前页面事实。 -- **F-017 提交围栏**:desk 端在真实点击前向 web 端申请提交围栏;web 端必须在一个原子 - 事务中复核任务版本、授权未消费、命令与演练关联正确,然后冻结授权并生成唯一 - `order_submission`。申请失败或响应不明确时不得点击。围栏成功后只能点击一次;点击结果 - 不明确时保留金额额度并进入调和,不能重新申请或重新点击。 -- **资金硬边界**:系统在任何路径下都不点击支付、免密支付、先用后付或任何扣款控件。 - 必须有测试证明提交订单之后不调用任何支付动作。 +- **F-017**:闸门二、闸门三和提交控件唯一均通过后,采购工具把当前验证摘要发给采购服务并申请 + 提交围栏。服务端原子复核任务版本、一次性授权未消费、领取身份、规格、数量、闸门值和唯一性, + 创建或重放唯一 `order_submission`,才返回 `click_permitted=true`。 +- 围栏申请失败、冲突、超时、断网或响应不明确时**不得点击**。围栏成功后只可精确点击一次 + “提交订单”;点击后无论结果、超时、外部支付交接或安全校验都禁止重试。 +- 明确创建订单后任务转 `WAITING_PAYMENT`;结果不明确转 `RECONCILIATION_REQUIRED`,授权和围栏 + 保持占用,只能调和同一提交记录。 +- 围栏建立前,授权过期、设备失败或人工放弃可使任务回到 `DRAFT`;必须由管理员重新点击“开始采购” + 产生新授权。围栏建立后不得释放、重新授权、重新领取或再次点击。 +- 系统任何路径都不点击支付、免密支付、先用后付或扣款控件;提交订单后的代码路径不含支付动作。 +- 发布真实下单能力前必须用独立真机任务完成只读 dry-run:到订单确认页验证规格、数量、金额和提交 + 控件唯一但不点击。dry-run 是版本发布门禁,不是每笔订单的第一趟,也不能代替实时三道闸门。 -### 结果与异常 +### 结果、证据与异常 -- **F-011**:失败必须可区分至少这些原因:设备未连接、商品页打不开、规格面板打不开、 - 规格不匹配、单价读不到、单价与授权价不符、数量设置失败、金额超上限、提交控件不唯一、 - 页面识别失败、安全校验、外部支付交接、超时。原始截图/页面快照只留采购工具本机隔离目录; - 脱敏成功时保留派生物,任何上传、远程审阅或服务端展示都只能使用自动复检通过的派生证据。 -- **版本失配**:运行时读取到的拼多多 App 版本与当前已取证版本不一致时,桌面端必须停止 - 领取真机任务并提示重新取证;不得继续使用旧页面判据。 -- **付款收口(MVP 简化版)**:订单创建后任务转「待付款」,页面展示订单截图、商品、 - 规格、数量和授权金额供人核对。**人在拼多多付款后手工标记完成。** 自动回读核对是 - F-016(V2)。 +- **F-007**:按步骤回传实际规格、两次规格面板价格、数量读回、确认页总额、判定、时间和截图。 + 截图是内部审计证据,不是提交前审批前置。 +- 内部系统允许上传规格面板与订单确认页的原始截图,截图中可保留页面已显示的地址和手机号;只允许 + 已认证设备显式上传、已登录管理员查看。完整 XML 只留采购工具本机,不上传;地址/手机号不解析为 + 业务字段或日志;外部支付页、支付凭据永不上传。 +- **F-011**:至少区分设备未连接、版本失配、商品打不开、面板打不开、规格不匹配、价格不可读、 + 两次价格不一致、数量复核失败、金额超上限、确认页不一致、提交控件不唯一、围栏失败、页面识别 + 失败、安全校验、外部支付交接和超时。 +- 运行时拼多多 App 版本与已取证版本不一致时停止领取真机任务,提示重新取证。 +- 订单明确创建后展示截图、商品、规格、数量和授权金额;人在拼多多付款后手工标记完成。 + **待付款不等于采购完成。** ### 通用 -- 每条 P0 判据关联至少一个 US 编号;有用户界面的判据同时关联相关 IX 编号。 -- 任务终态一次原子回写,中间态不落盘产生「看起来在跑其实已死」的记录。 +- 每条 P0 判据关联至少一个 US;有用户界面的判据同时关联 IX。 +- 状态流转与提交记录原子落盘,不产生“界面在跑但服务端无记录”的悬空事实。 +- 失败页面给出可执行下一步;不会把“重试”显示在已建立围栏的提交上。 ## 六、范围边界与决策 | 问题 | 决策 | | --- | --- | -| 第一版平台 | 网页端(管理)+ Windows 桌面端(执行),驱动一台 Android 手机 | -| 是否需要账号 | 是。管理员账号 + 桌面端设备凭据,两套身份分离 | -| 第一版范围 | 手工建单 → 批量开始试选 → 定时领取 → **第一趟试选** → 人工确认 → dry-run / 提交围栏 → **第二趟下单** → 待付款 | -| 任务来源 | **仅手工填链接。** Excel 与 ERP 移出 MVP | -| 找货方式 | **仅按链接。** 图片搜索移出 MVP | +| 第一版平台 | 采购服务(网页端)+ 采购工具(Windows 桌面端)+ 一台 Android 手机 | +| 身份 | 管理员账号 + 设备凭据,两套身份分离 | +| 第一版流程 | 手工建单 → 批量开始采购并授权 → 定时领取 → 单趟选规格 / 三闸门 / 围栏 / 提交一次 → 待付款 | +| 人工授权点 | 管理员点击“开始采购(只创建待付款订单)”;不再有试选后确认 | +| 任务来源 | 仅手工填链接;Excel 与 ERP 移出 MVP | +| 找货方式 | 仅按链接;图片搜索移出 MVP | | 采购平台 | 仅拼多多 | | 领取方式 | 定时轮询,只在执行员启动的会话内运行 | -| 任务启动 | 创建后为待开始;管理员在列表勾选一条或多条并统一开始第一趟试选 | -| 设备连接 | ADB over USB 或 WiFi 均支持;同一台手机不得同时以两种方式在线 | -| 付款收口 | 系统展示订单截图,人核对后付款并手工标记完成 | +| 设备连接 | ADB over USB 或 WiFi;同一手机不得以两种方式同时在线 | +| 付款收口 | 系统展示待付款订单证据,人核对、付款并手工标记完成 | | 暂不支持 | 自动付款、图搜、Excel、ERP、多设备并行、多平台、退款、审批链、AI 辅助 | ## 七、待确认 / 风险点 -- **第三方平台风险**:拼多多 App 版本更新会改变页面结构。前序项目已观察到详情页 - 没有独立规格入口、价格文本被拆成多个节点等变化。**每次页面结构判据都必须有真机 - 证据,不得从旧版本推断。** -- **资金风险**:涉及创建真实待付款订单。授权、金额上限、一次性围栏的规则由采购管理员 - 确认;付款始终人工。**下单动作在真机上第一次验证前,必须先取得授权。** -- **账号风险**:手机上是真实拼多多账号。频繁自动化操作有被风控或封号的可能,需要 - 可配置的动作节奏,并在检测到安全校验时立即停止。 -- **自动化边界风险**:会自动点击并创建订单,属不可逆操作。必须支持 dry-run(跑到 - 订单确认页停止)、服务端提交围栏和点击后调和;任一环节状态不明都不得继续点击。 -- **隐私风险**:规格面板和订单确认页都会显示收货地址和掩码手机号。允许原始 screenshot/XML - 仅在采购工具本机隔离目录短链路落盘供确定性脱敏器消费;业务逻辑、日志、agent、fixture 与采购 - 服务**不得提取或接收地址原文、手机号或支付凭据**。只有脱敏成功且自动复检通过的派生证据才能 - 上传或进入开发材料,无法确认脱敏完整即 fail closed。 -- **受控规格入口风险**:T-103 已证明当前衣服商品只有购买语义按钮能打开规格面板。项目所有者批准 - 仅把经真机取证的精确唯一入口作为第一趟可逆导航;该批准不覆盖其他文案,不授权调整数量、提交订单、 - 进入确认页或付款。T-103 必须用 capability 隔离和不可达测试证明边界没有扩散。 -- **规格面板上的单价位置未取证(阻塞 F-006 闸门一)**:选中 SKU 后价格显示在哪个节点、 - 是否带「券后」前缀、是否与原价并列,尚无本项目的真机证据。**T-103 必须一并取证。** - 若规格面板上无法可靠读到单价,闸门一要改为「只截图不判价」,确认页设计随之调整。 -- **两趟之间的状态漂移**:第二趟重新进入时价格可能已变、规格选项可能已改、商品可能 - 下架。闸门二负责拦截,一律转人工——但这意味着价格波动频繁的类目会产生大量待人工。 - 需真机观察实际发生率。 -- **定时轮询的节奏**:固定间隔的机器节奏比人工节奏更容易被风控识别。间隔需可配置, - 并在检测到安全校验时立即停止轮询。具体间隔待真机观察后确定。 -- **待确认(阻塞 F-002,已移出 MVP)**:Excel 表头契约的最终字段集与列名。 -- **待确认(阻塞 F-003,已移出 MVP)**:ERP 货运明细到任务颜色 / 尺码的字段映射。 -- **待确认(V2 之后)**:AI 辅助的模型供应商、调用预算与失败降级策略。 +- **第三方平台风险**:拼多多更新会改变页面结构。页面判据必须来自本项目真机证据,并绑定 App + 版本;不能从前序项目或旧版本推断。 +- **资金风险**:管理员点击开始采购会授权创建真实待付款订单。按钮必须明确影响范围与最高总额, + 服务端一次性授权、提交围栏和点击一次共同防止重复订单;付款始终人工。 +- **单趟时效风险**:授权到实际执行之间价格可能变化。任务不锁定旧观察价,而是在同一趟内两次读取 + 规格面板价格并在确认页校验总额;任何变化或歧义都停止。 +- **账号与风控风险**:固定机器节奏可能触发平台风控。轮询和动作间隔需可配置,检测到验证码、短信、 + 人脸或风险校验立即停止,不绕过。 +- **隐私风险**:内部原始截图可能含地址和手机号。只允许受控内部链路;不提取结构化隐私字段,完整 + XML 不上传,支付凭据与外部支付页截图禁止进入系统。 +- **首次真实提交风险**:生产提交能力启用前需独立 dry-run 和明确真机授权;点击后结果不明时只能调和。 +- **待确认(移出 MVP)**:Excel 表头、ERP 字段映射、AI 模型与预算。 diff --git a/docs/03-tech-stack.md b/docs/03-tech-stack.md index 83620d5..8141589 100644 --- a/docs/03-tech-stack.md +++ b/docs/03-tech-stack.md @@ -43,7 +43,7 @@ | 调用位置 | 采购工具 | 已定 | PC 有算力;改 prompt 不需要重新打包 | | provider | 待定 | **待定** | 需先确认预算与合规;不得由 agent 自行选定 | | 凭据存储 | 采购工具本机配置文件,不入库、不上传 | 已定 | 采购服务不保存、不代理、不下发任何模型凭据 | -| 输入 | 自动复检通过的脱敏派生 XML + 页面截图 | 已定 | 原始证据只允许本机确定性脱敏器消费,AI/agent 不读取原始地址或手机号 | +| 输入 | 待 V2 单独定义 | 待定 | MVP 不调用 AI;内部原始截图许可不自动扩大到外部模型 | ## 四、决策记录与演进 @@ -137,7 +137,7 @@ D:\Portable\adb\adb.exe devices -l 或发布半成品。 T-103 已证明当前衣服商品没有独立「规格/已选」入口。T-110 只批准拼多多 `8.17.0`、goods_id -`937122477375` 上经真机确认的精确唯一 `快要抢光` 作为可逆的受控规格面板入口;“免拼购买 / 单独购买 / +`937122477375` 上经真机确认的精确唯一 `快要抢光` 作为受控规格面板入口;“免拼购买 / 单独购买 / 直接拼成”等其他文案不能凭人工经验复用,必须分别重新取证。项目所有者已确认面板刚打开时目标颜色 “黑色CHA(纯棉)”和尺码“M(建议100-115)”均已自动选中;取证不强行取消选择,而是记录刚打开状态、 人工把两个维度都改成非目标值、再恢复目标值三个真实状态。只读取证 CLI 本身仍不执行点击;三种状态 @@ -156,16 +156,17 @@ PDD 规格面板不可避免显示收货区域和手机号。项目所有者确 T-103 已完成 `t103-privacy-v5` 的一次真实派生:人确认 XML 无实际地址/手机号、目标颜色 “黑色CHA(纯棉)”与尺码“M(建议100-115)”保留;安全节点显示顶部当前价“快卖完 ¥12.88”与原价 -“¥29.88”,底部“提交订单 ¥12.88”明确属于第一趟硬拒绝区。派生截图的整宽黑色隐私带遮住了顶部 -价格文字的一半,这只说明该截图不适合上传展示,不否定 XML 中已经确认的规格面板价格事实。 +“¥29.88”,底部“提交订单 ¥12.88”明确属于 T-103 隔离能力的硬拒绝区。派生截图的整宽黑色隐私带 +遮住了顶部价格文字的一半,这只说明旧脱敏实验不适合展示,不否定已确认的规格面板价格事实。 为加快 MVP,T-103 从此不再修改或调用截图遮罩器。现有 v5 派生 XML 只用于提取不含页面个人信息的 -最小 fixture;正式 `TrialSkuFlow` 在本机实时页面树上按拼多多 8.17.0 与面板结构读取允许字段,完整 +最小 fixture;T-111 完成后的 `SkuSelectionFlow` 在本机实时页面树上按拼多多 8.17.0 与面板结构读取允许字段,完整 页面树不落日志、不上传。T-204 直接接入原始截图上传,资产标记 `privacy_tier=INTERNAL_RAW`,只允许 已认证设备写入、已登录管理员读取;不上传 XML,不允许外部支付页或支付凭据。 -这个范围调整不授权第一趟调整数量、进入确认页、点击“提交订单”或触碰支付控件。最终仍由人把两个 -维度改成明确的非目标值,再运行受控入口、精确选择、读价和安全退出脚本完成真机验收。 +这个范围调整不授权 T-103 调整数量、进入确认页、点击“提交订单”或触碰支付控件。T-111 完成后, +T-103 以 `SkuSelectionFlow` 继续:由人把两个维度改成明确的非目标值,再运行受控入口、精确选择、 +读价和安全退出脚本完成真机验收。生产业务最终为单趟,但能力仍按 T-103 / T-105 / T-107 分段取证。 Windows 的标准入口是仓库根 `./init.ps1`。它要求 Go、两端目录及其哨兵文件存在;已有合规 `client/.venv` 时,所有采购工具检查与 validator 都使用该解释器。只有 venv 不存在时,才从 `py -0p` diff --git a/docs/04-architecture.md b/docs/04-architecture.md index 367ea44..e7fbdd2 100644 --- a/docs/04-architecture.md +++ b/docs/04-architecture.md @@ -1,482 +1,364 @@ # 架构设计 -> 本文讲「怎么把技术栈搭起来」:系统结构、职责划分、数据模型、技术难点、开发顺序。 -> 具体用了哪些框架 / 库 / 数据库 / 部署方式,见 [技术栈](03-tech-stack.md)。 +> 本文定义系统结构、职责、单趟采购流程、安全边界和数据模型。框架与运行命令见 +> [技术栈](03-tech-stack.md),双端线协议以 [API 合约](api.md)为唯一权威。 ## 一、系统结构 ```text -第三方 ERP(顺运宝) Excel 表格 人工填链接 - │ │ │ - └────────────┬───────────┴────────────────────┘ - v +人工填链接(MVP) Excel / ERP(V2) + │ │ + └──────────────┬───────────────┘ + v ┌─────────────────────────────────────────┐ - │ 采购服务(admin/,Go 单二进制) │ - │ · 建单、批量开始试选与任务生命周期 │ - │ · 候选确认与下单授权(唯一决策权威) │ - │ · 证据存储与审计 │ - │ · 管理 Web(服务端渲染) │ - └─────────────────────────────────────────┘ - ^ HTTP / JSON - │ Bearer Token + 设备绑定 - v + │ 采购服务(admin/,Go) │ + │ · 建单、查询、开始采购授权与任务状态 │ + │ · 提交围栏、结果调和、内部证据与审计 │ + │ · 服务端渲染管理页面 │ + └───────────────────┬─────────────────────┘ + │ HTTPS / JSON + │ Bearer + 设备绑定 + v ┌─────────────────────────────────────────┐ - │ 采购工具(client/,Python + PySide6) │ - │ · 领任务、跑流程、回传结果 │ - │ · 本地执行轨迹与证据落盘 │ - │ · AI 辅助调用(P1) │ - └─────────────────────────────────────────┘ - │ ADB(USB / WiFi) - v - ┌─────────────────────────────────────────┐ - │ Android 手机(拼多多 App) │ - └─────────────────────────────────────────┘ + │ 采购工具(client/,Python + PySide6) │ + │ · 轮询领取、单趟执行、回传状态与证据 │ + │ · 本地完整节点树与执行轨迹 │ + └───────────────────┬─────────────────────┘ + │ ADB(USB / WiFi) + v + Android 手机(拼多多 App) ``` -组件落位: - -- 采购服务:Go + gin,入口 `admin/cmd/server/main.go`,模板 - `admin/internal/transport/webui/templates/` -- 采购工具:Python,入口 `client/src/main.py`,真机流程 `client/src/android/pdd_flow.py` -- 数据库:SQLite,迁移由 goose 管理 -- 证据存储:采购服务本地文件系统,SHA-256 寻址 -- 外部服务:顺运宝 ERP(只读)、AI provider(P1,仅采购工具调用) +- 采购服务:Go + gin,SQLite,goose migration,本地 SHA-256 证据存储。 +- 采购工具:Python + uiautomator2 + PySide6;执行器只依赖 `TaskSource` / `ResultSink` 抽象。 +- 页面判据与拼多多 App 版本绑定;版本不同即停止,不把前序项目页面结构当作事实。 ## 二、职责划分 -### 采购服务(网页端,`admin/`) +### 采购服务(`admin/`) -**独占**: +独占以下权威: -- 任务的创建、状态流转和终态判定 -- 候选商品的接收与展示 -- **下单授权的签发与作废**——这是唯一的资金决策权威 -- 金额上限的判定 -- 证据资产的存储与访问控制 -- 管理员会话与设备凭据 +- 任务创建、批量开始采购、状态流转和终态判定; +- **一次性采购授权的签发**:管理员点击“开始采购”是唯一的人类授权动作; +- 任务不可变字段和最高总价校验; +- 提交订单前的原子围栏与点击后结果调和; +- 管理员会话、设备凭据、内部截图证据和审计记录。 -**不做**: +采购服务不连接手机、不发 ADB 命令、不解析拼多多页面,也不持有支付或 AI provider 凭据。 -- 不连接手机、不发 ADB 命令 -- 不保存、代理或下发任何 AI provider 凭据 -- 不解析拼多多页面 +### 采购工具(`client/`) -### 采购工具(桌面端,`client/`) +独占以下设备能力: -**独占**: +- ADB 连接、设备健康检查和已取证 App 版本校验; +- 打开商品、识别页面、精确选规格、设置数量、读取价格; +- 在满足全部门禁并取得服务端围栏后,精确点击一次“提交订单”; +- 截图、完整节点树和执行日志的本地采集,显式上传内部截图。 -- ADB 连接与设备健康检查 -- 拼多多页面识别、点击、选规格、设数量 -- 页面截图与节点树采集 -- AI 调用(P1) -- 完整执行轨迹的本地留档 - -**不做**: - -- **不自行决定买哪个候选**——必须等采购服务的授权 -- **不自行放宽金额上限**——本地校验只能更严,不能更松 -- 不直接读 Excel 或访问 ERP -- 不在没有授权的情况下执行任何创建订单的动作 +采购工具不自行修改任务约束、不扩大金额上限、不领取 `DRAFT`,也不能签发授权。没有服务端 +明确返回 `click_permitted=true` 时,任何本地判断都不能创建订单。 ### 权威冲突规则 -两端都会校验规格、数量、金额。**判定不一致时一律转人工,不取任一方结论。** -这条是硬规则:双闸门的价值在于分歧能被发现,自动选一边等于把双闸门降级成单闸门。 +服务端校验锁定的任务约束,客户端校验当前真机事实。任一端拒绝或两端摘要不一致,一律停止并 +转人工;不能为了“继续跑”选择相信其中一端。 -## 三、两趟执行 +## 三、单趟采购执行 -这是本项目最核心的结构决策。**MVP 只做 A 路径(任务自带商品链接),分两趟跑完。** +MVP 只做任务自带商品链接的 A 路径。创建任务与开始采购分离,但管理员开始后不再插入试选确认: ```text - ┌────────── 采购服务开始第一趟 ───────────┐ - │ 新任务先保存为 DRAFT │ - │ 管理员在任务表格勾选一条或多条 │ - │ 原子转为 PENDING,只进入试选队列 │ - └────────────────────┬─────────────────────┘ - v - ┌──────────────── 第一趟:试选 ────────────────┐ - │ 采购工具轮询领取 PENDING 任务 │ - │ 1. open_product(url) │ - │ 2. 经版本绑定、精确唯一的受控入口打开规格面板 │ - │ 3. 按维度精确勾选颜色分类、尺码 │ - │ 4. 【闸门一】读该 SKU 单价,算合计 │ - │ 5. 截图 │ - │ 6. 退出商品,释放手机 │ - │ 7. 回传标题 / 选中规格 / 单价 / 合计 / 截图 │ - └────────────────────┬─────────────────────────┘ - v - 任务转 WAITING_CONFIRMATION - │ - ┌────────────────────┴─────────────────────────┐ - │ 人在采购服务确认:机器选对了吗 │ - │ 看:需求 vs 选中规格、单价、合计、截图 │ - │ 点「确认下单(不付款)」→ 签发授权,锁定授权价 │ - │ 或「退回,不买」→ 任务终止 │ - └────────────────────┬─────────────────────────┘ - v - ┌──────────────── 第二趟:下单 ────────────────┐ - │ 采购工具轮询拿到授权 │ - │ 1. 重新 open_product(url) │ - │ 2. 重新按维度精确勾选同一规格 │ - │ 3. 【闸门二】重读单价,必须与授权价一致 │ - │ 4. 设数量并复核 │ - │ 5. 进订单确认页 │ - │ 6. 【闸门三】读「实付款」,不得超授权上限 │ - │ 7. 三个闸门全过 → 点一次「提交订单」 │ - │ 8. 回传订单截图 │ - └────────────────────┬─────────────────────────┘ - v - 任务转 WAITING_PAYMENT - │ - 人工在拼多多核对后付款 +管理员创建 DRAFT + │ + ├─ 勾选 DRAFT,查看选中数与最高总额 + └─ 点击“开始采购(只创建待付款订单)” + │ 同一事务:校验版本 + 创建一次性授权 + PENDING + v +采购工具领取授权任务 + │ 1. 打开 canonical 商品链接 + │ 2. 通过证据/版本绑定、精确唯一的受控入口打开规格面板 + │ 3. 按维度精确选择并读回颜色、尺码 + │ 4. 【闸门一】读 SKU 单价;单价×数量不得超过最高总价 + │ 5. 设置数量并精确读回 + │ 6. 【闸门二】重读规格与单价;规格不变且价格等于闸门一 + │ 7. 进入订单确认页 + │ 8. 【闸门三】规格/数量一致,应付总额不超最高总价 + │ 9. 上传验证摘要并申请服务端提交围栏 + │ 10. 仅在 click_permitted=true 且提交控件唯一时点击一次 + │ 11. 回传观察结果;不确定时只调和,不重试 + v +WAITING_PAYMENT ──人核对与付款──> SUCCEEDED + 或 +RECONCILIATION_REQUIRED ──人工核查同一提交──> WAITING_PAYMENT / FAILED ``` -### 为什么分两趟而不是停在面板上等人 +“开始采购”锁定的是管理员填写的 `goods_id`、颜色、尺码、数量和**最高总价**,不是一张旧页面 +截图里观察到的价格。价格在执行时实时读取,因此取消试选确认不会取消价格保护。 -一台手机是瓶颈。若第一趟停在规格面板等人确认,手机被占住跑不了别的任务,面板还可能 -超时或被拼多多重置。**第一趟必须退出并释放手机**,第二趟重新进入。 +### 受控规格面板入口 -代价是同一商品走两遍,但第二趟很快,而且换来两个好处:手机可以在人思考时继续跑别的 -任务的试选;价格变动能被第二趟抓住。 +T-103 真机证据表明:拼多多 `8.17.0`、goods_id `937122477375` 通过精确文本“快要抢光”打开 +规格面板。T-110 已批准把该**特定证据、版本和页面状态**绑定的点击定义为受控导航。 -### 第一趟受控规格面板入口 - -T-103 的真机证据推翻了“商品详情页存在独立规格入口”的假设:拼多多 8.17.0、goods_id -`937122477375` 只能从“快要抢光”打开规格面板。项目所有者于 2026-08-04 批准把这一点击定义为 -**可逆且能力受限的规格面板导航**,不把它当作下单授权,也不把购买语义文案整体加入白名单。 - -第一趟执行器只能持有以下 capability: - -1. 打开 canonical 商品链接。 -2. 点击与证据哈希、拼多多版本和页面状态绑定的精确唯一 `快要抢光` 入口。 -3. 在已确认的维度容器中精确选择规格并读回选中态。 -4. 从规格面板读取单价、保存原始截图、关闭面板并退出商品页。T-103 真机 spike 先在本机验证; - T-204 再把原始截图接入采购服务供管理员查看。 - -第一趟 capability **不得包含**通用 `click`、数量增减、进入订单确认页、提交订单或付款能力。 -“免拼购买 / 单独购买 / 直接拼成”等其他文案即使人工认为行为相同,也必须各自重新取证后才能评审; -入口缺失、重复、版本失配、打开后面板判据不唯一,或出现“提交订单”以外的未知终态按钮时立即停止。 -第一趟的静态依赖检查必须证明 `set_quantity()`、`go_to_order_confirm()`、`submit_order()` 和任何 -支付函数不可达。 +- 只能精确唯一匹配;缺失、重复、版本失配或打开后面板不唯一时零后续点击。 +- “免拼购买 / 单独购买 / 直接拼成”等其他文案不能用包含、前缀、同义或坐标兜底。 +- 受控入口只负责进入已取证面板,不等于支付授权,也不能暴露通用任意点击能力。 +- T-103 的隔离验证 capability 只包含开商品、开面板、选规格、读价和安全退出;数量、确认页、 + 提交和支付仍由后续真机任务分别取证后才能接入生产单趟执行器。 ### 三道价格闸门 -| 闸门 | 位置 | 作用 | 不通过时 | +| 闸门 | 当前页面 | 判据 | 拒绝条件 | | --- | --- | --- | --- | -| 一 | 第一趟规格面板 | 读该 SKU 单价,算合计,回传给人看 | 读不到即停,转人工 | -| 二 | 第二趟规格面板 | 重读单价,**必须与授权时锁定的价格一致** | 不一致即停,转人工 | -| 三 | 订单确认页 | 读「实付款」,不得超授权总额上限 | 超出即停,转人工 | +| 一 | 规格面板,选中目标规格后 | 单价唯一可读;`单价 × 授权数量 <= 最高总价` | 不可读、有歧义或超上限 | +| 二 | 规格面板,数量读回后 | 颜色、尺码仍正确;重读单价与闸门一完全相等 | 规格漂移或价格变化 | +| 三 | 订单确认页 | 规格、数量正确;应付总额唯一可读且不超最高总价 | 任一不一致、不可读或超上限 | -**闸门二不可省略。** 人确认的是「32.50 元这一单」,不是「这个商品」。拼多多价格波动 -常见,不能因为「人已经确认过」就照下不误。 +金额一律使用十进制字符串和十进制定点运算,不用浮点数。价格只从规格面板和订单确认页读取; +详情正文、搜索卡片、底部购买/提交按钮的数字不作为价格来源。 -**价格只在规格面板和订单确认页读。** 商品详情页正文和搜索结果卡片上的价格文本在真机上 -被拆成多个节点(`¥` 与数字分离)、带 `券后` 一类前缀、实付价 / 原价 / 促销价难以区分 -——前序项目在这里耗掉大量时间且无可靠结论。 +闸门一与闸门二发生在同一设备会话中。它们用于发现选择或设置数量造成的页面变化,不需要管理员 +在中间确认。旧截图、缓存值和发布前 dry-run 都不能替代这次实时读取。 -### B 路径(图片搜索)——V2,MVP 不做 +### B 路径(V2) -任务只有参考图、没有链接时,需要先搜图找出商品。**该路径推迟到 V2**,MVP 阶段建单必须 -提供商品链接。 - -V2 实现时仍遵守:**图搜的唯一产出是 goods_id**,不在搜索结果卡片上读价格或据价筛选, -拿到 goods_id 后汇入本节的两趟流程。 +没有商品链接时由图片搜索只产出 `goods_id`,再汇入上述流程。不在搜索结果页读取价格或规格。 ## 四、安全边界(硬约束) -以下每一条都必须有单元测试证明,且不得在任务中「顺手放宽」。 +以下每条都必须有测试证明,不得在任务中顺手放宽: -| 边界 | 规则 | 违反后果 | +| 边界 | 规则 | 防止什么 | | --- | --- | --- | -| 不付款 | 任何路径都不点击支付、免密支付、先用后付或扣款控件 | 真实资金损失 | -| **提交订单四条件** | 见下方专节。四者缺一不可,且**只允许点击一次** | 误下单 / 重复下单 | -| 订单确认页其余零点击 | 除「提交订单」与返回外,不点击确认页上任何控件 | 误触发未知动作 | -| 规格精确匹配 | 按维度等值匹配,防前缀碰撞(`红`/`粉红`、`1`/`10`);找不到即停 | 买错货 | -| 提交订单控件唯一 | 文本精确等于「提交订单」且可点击祖先唯一,否则停 | 点到未知控件 | -| 数量必须复核 | 设置后读回确认精确等于要求值,否则停 | 买错数量 | -| 价格三道闸门 | 见第三节。任一道读不到或不通过即停,**不用其他位置的数字凑合** | 超预算采购 | -| 第一趟不下单 | 只允许点击证据/版本绑定的精确唯一受控入口打开规格面板,当前仅为 `快要抢光`;随后只选规格、读价、截图和返回。数量、确认页、`提交订单`、付款与通用点击能力均不可达 | 无授权下单 | -| 外部支付页 | 检测到微信等外部支付交接立即停止、转人工、保留证据 | 凭据泄露 | -| 安全校验 | 检测到验证码、风控、人脸、短信校验立即停止,不尝试绕过 | 封号 / 违规 | -| 页面个人信息 | 内部原始截图可包含并上传页面已显示的地址、手机号,供已登录管理员查看;不得把这些内容解析成业务字段或写入普通日志、Git、Vikunja。支付凭据仍不得读取或上传 | 非必要扩散 / 凭据泄露 | -| 授权一次性 | 一笔授权只能产生一笔订单,重复提交幂等 | 重复采购 | -| 服务端提交围栏 | 真机点击前必须由采购服务原子冻结授权并创建唯一提交记录;失败或响应不明不得点击 | 并发 / 断网导致重复下单 | -| App 版本失配即停 | 运行版本与本项目已取证版本不一致时停止领取真机任务,先重新取证 | 旧判据误点新页面 | +| 不付款 | 不点击支付、免密支付、先用后付或任何扣款控件 | 真实资金损失 | +| 提交四条件 | 授权+围栏、闸门二、闸门三、控件唯一同时成立,只点一次 | 误下单 / 重复下单 | +| 确认页零点击 | 除返回和满足四条件后的“提交订单”外不点击任何控件 | 未知副作用 | +| 规格精确匹配 | 维度内等值唯一匹配,防 `红/粉红`、`1/10` 前缀碰撞 | 买错规格 | +| 数量读回复核 | 设置后精确读回,不一致即停 | 买错数量 | +| 三道价格闸门 | 任一道不可读、有歧义或不通过都停,不用别处数字凑 | 超预算 | +| 受控页面能力 | 页面动作按任务与证据分层;不得把通用 `click` 传入业务流程 | 边界扩散 | +| 外部支付页 | 检测到外部支付交接立即停止,不读取、保存或输入凭据 | 凭据泄露 | +| 安全校验 | 验证码、风控、人脸、短信出现即停止,不绕过 | 封号 / 违规 | +| 内部截图 | 可上传页面已显示的地址/手机号;不解析成字段或日志,完整 XML 不上传 | 非必要扩散 | +| 授权一次性 | 一条任务版本只有一份有效授权;幂等重放不生成第二份 | 重复采购 | +| 服务端提交围栏 | 点击前原子创建唯一提交记录;失败或响应不明不得点击 | 并发 / 断网重复下单 | +| App 版本绑定 | 运行版本不同于证据版本时停止并重新取证 | 旧判据误点 | ### 提交订单的四个前置条件 -这是本项目唯一会创建真实待付款订单的动作。**四者同时满足才允许点击,且只点一次:** +“提交订单”是系统唯一会创建真实待付款订单的动作。以下四项同时满足才允许点击一次: -1. **授权存在且未消费,并已建立服务端提交围栏**——采购服务已签发、采购工具已 ack; - 真机点击前,采购服务在一个原子事务中把授权从可执行态冻结为本次唯一 - `order_submission`。围栏接口失败或响应不明时不得点击。 -2. **闸门二通过**——第二趟重读的单价与授权时锁定的价格一致。 -3. **闸门三通过**——订单确认页「实付款」不超过授权总额上限。 -4. **控件唯一**——文本精确等于「提交订单」且可点击祖先唯一。 +1. **一次性授权有效且服务端提交围栏已建立**:围栏把授权、任务、领取和本次验证摘要原子绑定到 + 唯一 `order_submission`;明确响应包含 `click_permitted=true`。 +2. **闸门二通过**:目标规格未漂移,第二次规格面板单价等于第一次。 +3. **闸门三通过**:确认页规格、数量正确,应付总额不超过授权最高总价。 +4. **提交控件唯一**:文本精确等于“提交订单”,可点击祖先唯一。 -点击之后,**无论发生什么都不重试**: +围栏请求超时、断网、冲突或响应不明时不得点击。围栏建立后: -| 点击后观察到 | 处置 | +| 观察结果 | 处置 | | --- | --- | -| 正常进入订单结果页 | 回传订单截图,任务转 `WAITING_PAYMENT` | -| 跳转微信等外部支付 | 立即停止,转人工,提示「订单可能已创建、支付未完成」 | -| 安全校验 | 立即停止,转人工,保留证据 | -| 超时或页面无法判定 | 转人工,**预留金额额度**,提示订单状态不明 | +| 明确进入订单结果 / 待付款页 | 上报 `SUBMITTED`,任务转 `WAITING_PAYMENT` | +| 跳转外部支付 | 立即停止,上报结果不明确,不执行支付 | +| 出现验证码 / 风控 / 人脸 / 短信 | 立即停止,上报结果不明确,不绕过 | +| 超时、断连、页面无法判定 | 转 `RECONCILIATION_REQUIRED`,保留围栏和金额额度 | -后三种情况一律**禁止自动重试点击**。授权保持永久围栏,结果明确后再记为已消费;在此之前 -也绝不能重新开放——宁可人工核实一遍,不可能重复下单。 +后三种情况都禁止释放围栏、重新授权、重新领取或再次点击。只能调和同一提交记录。 -### dry-run、提交围栏与结果调和 +### 发布前 dry-run 与生产围栏的区别 -下单被拆成三个不可逆程度不同的阶段,任何客户端本地判断都不能替代服务端围栏: - -1. **dry-run(只读演练)**:进入订单确认页,读取规格、数量和「实付款」,确认提交控件 - 唯一,上传证据后退出。该阶段绝不点击「提交订单」,也不消费授权。 -2. **提交围栏**:真实第二趟再次读取并通过三道闸门后,采购工具向采购服务申请围栏。采购服务 - 原子校验任务版本、命令、未消费授权和唯一性,创建 `order_submissions` 记录并冻结授权。 - 只有明确收到成功响应,采购工具才可点击一次。 -3. **结果调和**:点击后只上报观察结果。明确创建则转 `WAITING_PAYMENT`;超时、外部支付、 - 安全校验或断连均转 `RECONCILIATION_REQUIRED`,保留额度并由人核查。**不得释放围栏、 - 重新签发授权或自动重试点击。** - -授权超时和主动放弃只允许发生在提交围栏建立之前。围栏之后即使租约过期,也只能恢复 -同一提交记录并进入调和,不能把任务重新放回可领取队列。 +首次启用某一 App 版本的真实提交能力前,必须用独立真机任务完成只读 dry-run:进入确认页,验证 +规格、数量、金额与提交控件唯一,然后退出且不点击。它用于证明判据和不可达测试,不是每笔采购的 +“第一趟”,也不产生可复用页面事实。生产任务仍在同一趟内重新通过三道闸门并申请服务端围栏。 ## 五、数据模型 ### 5.1 核心实体 ```sql --- 采购任务:创建后业务约束不可变 CREATE TABLE tasks ( - id TEXT PRIMARY KEY, -- UUID - source TEXT NOT NULL, -- MANUAL | EXCEL | ERP - source_ref TEXT, -- 外部单号(如虾皮订单号),非内部 id - title TEXT NOT NULL, - goods_id TEXT NOT NULL, -- MVP 必填;B 路径(可为空)推迟到 V2 - sku_color TEXT NOT NULL, - sku_size TEXT NOT NULL, - quantity INTEGER NOT NULL CHECK (quantity > 0), - max_total_price TEXT NOT NULL, -- 十进制字符串,资金边界不得为空 - reference_asset_id TEXT, -- 参考图,MVP 可选;B 路径(V2)必填 - status TEXT NOT NULL, - version INTEGER NOT NULL DEFAULT 1,-- 乐观锁 - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL -); - --- 第一趟试选结果:人做确认决策的依据 -CREATE TABLE spec_trials ( id TEXT PRIMARY KEY, - task_id TEXT NOT NULL REFERENCES tasks(id), - attempt INTEGER NOT NULL, - product_title TEXT NOT NULL, -- 商品页读到的标题 - selected_color TEXT NOT NULL, -- 实际勾选到的颜色分类 - selected_size TEXT NOT NULL, -- 实际勾选到的尺码 - unit_price TEXT NOT NULL, -- 闸门一读到的单价 - total_price TEXT NOT NULL, -- unit_price × quantity - evidence_sha256 TEXT NOT NULL, -- 规格面板截图 + source TEXT NOT NULL, -- MANUAL | EXCEL | ERP + source_ref TEXT, + title TEXT NOT NULL, + goods_id TEXT NOT NULL, + sku_color TEXT NOT NULL, + sku_size TEXT NOT NULL, + quantity INTEGER NOT NULL CHECK (quantity > 0), + max_total_price TEXT NOT NULL, -- 十进制字符串 + reference_asset_id TEXT, + status TEXT NOT NULL, + version INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, - UNIQUE (task_id, attempt) + updated_at TEXT NOT NULL ); --- 下单授权:唯一的资金决策记录 +-- 管理员点击“开始采购”产生;锁定任务约束,不锁定旧观察价 CREATE TABLE order_authorizations ( id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id), - spec_trial_id TEXT NOT NULL REFERENCES spec_trials(id), - version INTEGER NOT NULL, + task_version INTEGER NOT NULL, + start_key TEXT NOT NULL, goods_id TEXT NOT NULL, sku_color TEXT NOT NULL, sku_size TEXT NOT NULL, quantity INTEGER NOT NULL, - authorized_unit_price TEXT NOT NULL, -- 锁定价:闸门二据此比对 - total_price_cap TEXT NOT NULL, -- 授权总额上限:闸门三据此比对 - note TEXT, -- 可选备注 - status TEXT NOT NULL, + total_price_cap TEXT NOT NULL, + status TEXT NOT NULL, -- ACTIVE | CLAIMED | FENCED | CONSUMED | EXPIRED | ABANDONED created_by TEXT NOT NULL, created_at TEXT NOT NULL, - expires_at TEXT NOT NULL, -- 围栏前超时自动作废;围栏后不再释放 - UNIQUE (task_id, version) + expires_at TEXT NOT NULL, + UNIQUE (task_id, task_version), + UNIQUE (start_key, task_id) ); --- 真实点击前的服务端一次性围栏;一笔授权最多一条 +-- 一次领取产生一条可恢复执行;保存步骤摘要,不接收完整 XML +CREATE TABLE purchase_attempts ( + id TEXT PRIMARY KEY, + task_id TEXT NOT NULL REFERENCES tasks(id), + authorization_id TEXT NOT NULL REFERENCES order_authorizations(id), + claim_generation INTEGER NOT NULL, + status TEXT NOT NULL, + gate1_unit_price TEXT, + gate2_unit_price TEXT, + quantity_read INTEGER, + confirm_amount TEXT, + failure_code TEXT, + started_at TEXT NOT NULL, + finished_at TEXT, + UNIQUE (task_id, claim_generation) +); + +-- 真机真实点击前建立;一份授权最多一条 CREATE TABLE order_submissions ( id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id), authorization_id TEXT NOT NULL REFERENCES order_authorizations(id), - command_id TEXT NOT NULL, - dry_run_id TEXT NOT NULL, - status TEXT NOT NULL, -- FENCED | SUBMITTED | RECONCILIATION_REQUIRED | MANUAL_RESOLVED - verified_unit_price TEXT NOT NULL, - quantity_read INTEGER NOT NULL, - confirm_page_amount TEXT NOT NULL, - created_at TEXT NOT NULL, - resolved_at TEXT, + attempt_id TEXT NOT NULL REFERENCES purchase_attempts(id), + status TEXT NOT NULL, -- FENCED | SUBMITTED | RECONCILIATION_REQUIRED | MANUAL_RESOLVED + gate1_unit_price TEXT NOT NULL, + gate2_unit_price TEXT NOT NULL, + quantity_read INTEGER NOT NULL, + confirm_amount TEXT NOT NULL, + created_at TEXT NOT NULL, + resolved_at TEXT, UNIQUE (authorization_id), - UNIQUE (command_id) + UNIQUE (attempt_id) ); ``` -`authorized_unit_price` 是第二趟闸门二的比对基准,**必须来自人确认时看到的那个试选 -结果**,不能在签发时重新取值。`expires_at` 见 5.3 节。 - -MVP 的授权没有「选择理由 / 拒绝理由」——那是从多个候选里挑一个时的留档需求。这里人 -只回答「机器选对了吗」,保留一个可选 `note` 即可。 - -金额一律用**十进制字符串**存储和传输,不用浮点数。 +MVP 不再用 `spec_trials` 作为审批记录,也不存在 `authorized_unit_price`。实际读价属于 +`purchase_attempts` / `order_submissions` 的执行与审计事实;管理员授权的资金边界始终是 +`total_price_cap`。 ### 5.2 状态机 -任务状态(采购服务权威)。创建与开始试选分离;两趟执行对应两次 `CLAIMED → RUNNING`: - ```text -DRAFT ─start trial→ PENDING ─┐ -PENDING_RETRIAL ─────────────┴─claim→ CLAIMED ─start→ RUNNING(TRIAL) - ↑ │ ├→ NEEDS_MANUAL - └────────release──────┘ └→ WAITING_CONFIRMATION - ├→ CANCELED - └→ AUTHORIZED - └claim→ ORDERING - ├→ NEEDS_MANUAL(围栏前失败) - └→ [submission FENCED] - ├→ WAITING_PAYMENT - │ └→ SUCCEEDED - └→ RECONCILIATION_REQUIRED - └→ 人工核查 / 调和 +DRAFT + └─开始采购(创建授权)→ PENDING + └─claim→ CLAIMED ─start→ ORDERING + ├─围栏前验证失败→ NEEDS_MANUAL ─人工处理/重置→ DRAFT + ├─围栏前授权过期/安全释放→ DRAFT + └─submission FENCED + ├─明确创建→ WAITING_PAYMENT ─人工付款并标记→ SUCCEEDED + └─结果不明→ RECONCILIATION_REQUIRED + └─人工调和同一提交→ WAITING_PAYMENT / FAILED + +DRAFT / PENDING / NEEDS_MANUAL ─管理员取消(围栏前)→ CANCELED ``` -| 状态 | 含义 | +| 状态 | 含义与安全下一步 | | --- | --- | -| `DRAFT` | 已保存、等待管理员开始试选;设备不可领取,也不存在下单授权 | -| `RUNNING(TRIAL)` | 第一趟试选中:正在勾选规格、读价、截图 | -| `WAITING_CONFIRMATION` | 试选已回传,**等人确认机器选对了没** | -| `PENDING_RETRIAL` | 旧授权已过期或在围栏前被放弃,必须重新跑第一趟取得新价格 | -| `AUTHORIZED` | 已签发授权,等采购工具下一轮轮询领走 | -| `ORDERING` | 第二趟下单中:重新选规格、过闸门二三、提交订单 | -| `WAITING_PAYMENT` | 订单已创建,等人在拼多多付款。**这不是成功** | -| `RECONCILIATION_REQUIRED` | 已建立提交围栏,但点击结果不明确;可能已创建订单,只能核查,不能重试 | -| `NEEDS_MANUAL` | 围栏前的转人工情形(规格不匹配、价格不符、页面识别失败等) | -| `SUCCEEDED` | 订单已付款且核对通过 | +| `DRAFT` | 已保存,未授权;管理员可编辑/取消或点击开始采购;设备不可领取 | +| `PENDING` | 已有有效一次性授权,等待采购工具领取 | +| `CLAIMED` | 已由一个设备实例持有租约,尚未开始页面操作 | +| `ORDERING` | 单趟执行中,正在选规格、过闸门或申请围栏 | +| `NEEDS_MANUAL` | 围栏前失败;显示原因,由人核查后重置为 DRAFT 或取消 | +| `WAITING_PAYMENT` | 订单已明确创建,等待人在拼多多付款;**不是成功** | +| `RECONCILIATION_REQUIRED` | 围栏后结果不明;只能核查同一提交,不能重试 | +| `SUCCEEDED` | 人已付款并完成核对 | +| `FAILED` | 人工调和确认订单未创建或任务无法完成 | +| `CANCELED` | 围栏前由管理员取消,不再执行 | -授权状态:`PENDING_DELIVERY → DELIVERED → ACKNOWLEDGED → EXECUTING → FENCED → CONSUMED`。 -人退回或重新确认时旧授权转 `SUPERSEDED`;超时转 `EXPIRED`。 +批量 `DRAFT → PENDING` 必须全有或全无。服务端同时创建授权;“先改状态、稍后补授权”无效。 +`WAITING_CONFIRMATION`、`PENDING_RETRIAL`、`AUTHORIZED` 和 `RUNNING(TRIAL)` 不再属于 MVP 状态。 -`DRAFT → PENDING` 只能由管理端“开始试选”动作触发。批量开始在一个事务中校验全部任务仍为 -`DRAFT` 且版本一致后统一流转;任一冲突时整批不变,避免用户误以为选中的任务都已开始。 -这个动作只开放第一趟领取资格,不创建 `order_authorizations` 或 `order_submissions`。 +### 5.3 授权、租约与恢复 -### 5.3 授权超时(MVP 必做,不得推后) - -> **前序项目的教训**:曾出现 `EXECUTING` 授权永不推进,导致确认表单被永久隐藏、任务 -> 锁死,只能新建任务绕过。 - -规则: - -- 每笔授权带 `expires_at`。**仅在尚未建立提交围栏时**,超时自动转 `EXPIRED`。 -- 授权 `EXPIRED` 后任务转 `PENDING_RETRIAL`,先重新跑第一趟取得新价格,再回到人工确认; - 不允许在旧 `spec_trials` 上直接重新确认。 -- 采购服务在围栏建立前提供「放弃当前授权」入口;围栏建立后改为「进入人工核查」,不得 - 作废或释放授权。 -- **任何时候都不允许出现「任务停在某状态且界面上没有任何可用动作」的组合。** - 这是验收项,不是实现细节。 - -授权过期后重新确认时,必须重新走第一趟试选取得新的 `spec_trials` 记录——不能复用旧的 -锁定价,因为价格可能已经变了。已建立围栏的授权不参与本超时流程。 +- 授权带 `expires_at`,只有围栏前可转 `EXPIRED` / `ABANDONED`;任务回到 `DRAFT`,必须重新点击 + 开始采购。旧授权永不复活。 +- 设备租约丢失不等于授权可安全重用。只有服务端确认该 attempt 未建立围栏,才能关闭 attempt 并 + 回到 `DRAFT` / `NEEDS_MANUAL`;不能自动重新领取并重复页面动作。 +- 围栏建立后即使租约过期也只恢复同一 `order_submission` 的调和,不能回到可领取队列。 +- 每种非终态都必须给出安全下一步,不能出现隐藏表单导致任务永久锁死。 ### 5.4 证据分层 -| 数据 | 位置 | 理由 | +| 数据 | 位置 | 边界 | | --- | --- | --- | -| 原始商品页 / 规格页 screenshot | 采购工具本机 + 采购服务内部证据存储(T-204) | 内部系统允许保留页面中已显示的地址和手机号;设备鉴权后上传,只有已登录管理员可查看,不做遮罩或裁剪 | -| 原始商品页 / 规格页完整 XML | **仅采购工具本机隔离目录** | 自动化运行时在内存中读取规格/价格,完整 XML 不上传、不进入日志、Git、Vikunja 或 fixture | -| T-103 真机 spike 证据 | 先在采购工具本机;T-204 接入上传 | T-103 只验证选择与读价,不实现 HTTP 证据链;这是一项任务拆分,不是禁止原始截图上传 | -| 最小脱敏 XML fixture | 采购工具测试 / 可提交 Git | 只保留页面判据所需结构;自动复检无地址、手机号、支付凭据后才可发布 | -| 原始订单确认页截图 | 采购服务内部证据存储 | 授权后核对与审计必须留;允许页面已显示的地址/手机号,禁止外部支付凭据 | -| 原始订单核对截图 | 采购服务内部证据存储 | 资金核对证据;只有已认证设备上传、已登录管理员查看 | -| AI 调用记录(P1) | **仅采购工具本地** | 含 prompt / 响应全文,脱敏成本高 | -| 失败现场快照 | 原始物仅采购工具本机;只能手工导出脱敏派生物 | 同上 | +| 商品 / 规格 / 确认页原始 screenshot | 采购工具本机 + 采购服务内部证据存储 | 可含页面已显示地址/手机号;设备鉴权上传、管理员登录查看,不遮罩 | +| 完整 XML | 仅采购工具本机隔离目录 | 可在内存解析页面判据;不上传、不写日志、Git、Vikunja | +| 最小 XML fixture | 采购工具测试 / Git | 只保留判据所需结构,确认无地址、手机号、支付凭据 | +| 外部支付页或支付凭据 | 不保存、不上传 | 检测到交接立即停止 | +| AI 调用记录(V2) | 仅采购工具本地 | 不进入采购服务 | -截图上传器只能接收调用方显式传入的截图文件,不能枚举整个原始目录,也不能上传 XML、manifest 中的 -本机路径或其他文件。采购工具可以在真机流程内存中读取当前页面树,但只返回规格、选中态、价格和页面 -状态摘要;不得把地址或手机号解析成结构化业务字段。原始 XML 不进入 HTTP、日志、Vikunja、Git 或 -fixture;仓库只允许进入与页面判据有关的最小 XML fixture。 - -T-204 负责原始截图上传、SHA-256 校验、内部访问控制和保留策略,不再实现遮罩或裁剪。证据响应使用 -`Cache-Control: no-store`,不得暴露为无需登录的静态目录。外部支付页及任何支付凭据不属于“内部原图 -可上传”的范围,遇到支付交接仍立即停止。 +截图上传器只能接收调用方显式指定的截图,不能枚举证据目录或顺带上传 XML/manifest。证据响应 +使用 `Cache-Control: no-store`,不能暴露为免登录静态目录。 ## 六、关键技术难点 -| 难点 | 说明 | 应对 | +| 难点 | 风险 | 应对 | | --- | --- | --- | -| 拼多多页面结构随版本变化 | 前序项目已观察到详情页无独立规格入口、价格节点拆分等变化 | **每条判据先做真机 spike 取证再写代码**;判据与 App 版本一并记录 | -| 规格面板安全入口 | T-103 已在拼多多 8.17.0 真机确认衣服商品只能通过购买语义入口打开规格面板 | T-110 已批准仅使用当前证据证明的精确唯一 `快要抢光` 作为受控导航;其他文案不泛化,第一趟下单能力保持不可达 | -| 规格面板上的价格位置 | 选中 SKU 后价格显示在哪、是否含券后前缀,未取证 | **T-103 必须一并取证**,闸门一依赖它;读不到就转人工,不用详情页数字凑合 | -| 同一商品两趟结果不一致 | 第二趟价格变了、规格选项变了或商品下架 | 闸门二拦截;一律转人工,不自动放弃也不自动继续 | -| 图搜结果含跨类目商品(V2) | 搜服装出现纸巾 | B 路径只产 goods_id 且限 5 个;后续用 VLM 看截图筛同款 | -| WiFi ADB 稳定性 | 息屏、换网、DHCP 续租会断连 | 超时可配置;断连视为技术失败并保留现场,不重试点击 | -| 同一手机 USB + WiFi 同时在线 | `adb devices` 列出两条,自动选设备会失败 | serial 必填;多在线通道必须读到 `ro.serialno` 或 `ro.boot.serialno` 才能比对。身份一致或任一身份读取失败时都 fail closed,不能以相同 model/product 猜测后继续 | -| 不可逆动作的重试 | 点击「现在买」后超时,无法判断订单是否已创建 | 一律转人工并预留金额额度,**禁止自动重试点击** | -| 双端契约漂移 | 两端独立演进会静默不兼容 | 契约改动必跑完整门禁;[api.md](api.md) 是唯一权威 | - -**高风险功能先做最小原型。** Phase 1 的真机 spike 必须先于 Phase 2 的界面开发完成。 +| 页面结构随版本变化 | 旧选择器误点新页面 | 每条判据先真机取证,记录 App 版本、截图、XML、goods_id | +| 购买语义入口才打开面板 | 能力范围易扩散 | 只批准证据绑定的精确唯一入口;每种文案单独取证 | +| 当前价与原价/按钮价混杂 | 读错价格 | 限定已取证结构和语义;价格只在面板/确认页读,歧义即停 | +| 单趟页面状态变化 | 数量或促销导致价格变化 | 同一趟两次读规格面板价,再以确认页总额兜底 | +| WiFi ADB / 双通道 | 断连或操作错设备 | serial 必填;USB/WiFi 同设备或身份不明时 fail closed | +| 不可逆动作超时 | 可能已创建订单 | 服务端围栏 + 点击一次 + 只调和,不重试 | +| 双端契约漂移 | 静默不兼容 | [api.md](api.md) 唯一权威;契约改动跑完整双端门禁 | ## 七、推荐开发顺序 -1. **Phase 0 地基**:两端骨架、测试命令、`init` 脚本可运行。 -2. **Phase 1 真机取证**:WiFi ADB 连通;先验证第一趟的打开商品 → 受控打开规格面板 → 按维度 - 精确勾选颜色分类和尺码 → **读到该 SKU 单价** → 原始截图取证 → 退出;再以独立的第二趟 spike - 验证设数量 → 进订单确认页 → 读「实付款」。第一趟 capability 不含后三项。 - **结论写入文档,判据带拼多多 App 版本。** -3. **Phase 2 采购服务核心**:数据模型与状态机、手工建单、任务查询、试选结果接收、 - 确认页与授权签发、**授权超时与放弃**。 -4. **Phase 3 双端打通**:设备侧 API、采购工具 `HttpTaskSource`/`HttpResultSink`、 - 定时轮询、第一趟试选端到端。 -5. **Phase 4 闭环收尾**:第二趟下单(含三道闸门与提交)、失败分类、完整验收、打包。 -6. **V2 及以后**:图片搜索路径、候选对照台、Excel 导入、ERP 建单、订单自动核对、AI 辅助。 +1. **Phase 0 地基**:双端骨架、测试命令、初始化脚本和原型。 +2. **Phase 1 真机取证**:逐段验证商品打开、受控面板入口、精确规格与读价、数量、确认页和提交 + 控件;真实点击前先完成独立 dry-run。每个 spike 的 capability 只覆盖当期动作。 +3. **Phase 2 采购服务核心**:DRAFT 建单/列表、批量开始采购与授权、状态/证据/围栏/调和接口。 +4. **Phase 3 双端打通**:设备身份、原子领取、单趟执行到围栏前、事件与截图。 +5. **Phase 4 闭环**:经明确真机授权验证一次提交、待付款收口、失败分类、打包。 +6. **V2**:图搜、Excel、ERP、自动核对、AI、多设备。 -**不要在 Phase 1 结论出来之前写 Phase 2 的页面**——确认页要显示什么,取决于真机上 -究竟能读到什么。尤其是闸门一的单价,如果规格面板上读不可靠,整个确认页的设计要改。 - -> 2026-08-04:Phase 1 证明已验证衣服商品没有独立规格入口。项目所有者随后批准 T-110 的受控入口 -> 方案:当前只允许证据绑定的精确唯一 `快要抢光` 打开规格面板,并以 capability 隔离保证数量、确认页、 -> 提交订单和付款在第一趟不可达。项目所有者随后决定加速 MVP:停止截图遮罩器开发,T-103 使用现有 -> 安全最小 XML 继续实现选择与读价;T-204 直接把内部原始截图上传采购服务,不遮罩地址或手机号。 +T-103 继续作为“规格选择与读价”的隔离前置,不含数量、确认页或提交。生产单趟并不意味着在一个 +任务里跳过逐段取证;它只意味着这些已验证能力集成后,每笔业务任务不再等待中途人工确认。 ## 八、项目结构 ```text cmbuyer/ ├── docs/ -├── admin/ # 采购服务(Go) +├── admin/ │ ├── cmd/server/ -│ ├── internal/ -│ │ ├── domain/ # 实体与状态机,无外部依赖 -│ │ ├── usecase/ # 业务用例 -│ │ ├── transport/ -│ │ │ ├── httpapi/ # 设备侧 API -│ │ │ └── webui/ # 管理页面 + 模板 + 静态资源 -│ │ └── storage/ # SQLite 与证据资产 +│ ├── internal/domain/ +│ ├── internal/usecase/ +│ ├── internal/transport/httpapi/ +│ ├── internal/transport/webui/ +│ ├── internal/storage/ │ └── migrations/ -├── client/ # 采购工具(Python) -│ ├── src/ -│ │ ├── android/ # adb / device / pdd_flow -│ │ ├── core/ # models / task_runner / sources 抽象 -│ │ ├── remote/ # HttpTaskSource / HttpResultSink -│ │ └── app/ # PySide6 GUI +├── client/ +│ ├── src/cmbuyer_client/device/ +│ ├── src/cmbuyer_client/pdd/ +│ ├── src/cmbuyer_client/core/ +│ ├── src/cmbuyer_client/remote/ +│ ├── src/cmbuyer_client/app/ │ └── tests/ └── scripts/ ``` -`client/src/core/sources.py` 必须保留 `TaskSource` / `ResultSink` 抽象,执行器只依赖抽象。 -这样离线 Excel 模式可作为降级路径存在,且执行器不因来源变化而改动。 +执行器依赖 `TaskSource` / `ResultSink`,不直接读取 Excel 或拼接 HTTP。来源变化不得改变安全执行器。 ## 九、架构纪律 -- 业务事实和 schema 变化必须同步更新本文与 [api.md](api.md)。 -- 不在代码里发明文档没有的接口、字段和状态。 -- 第四节的安全边界不得在任务中放宽;确需变更时先改本文并说明理由。 -- 高风险模块先单独真机验证,再接入完整流程。 -- 前序项目 `cmroubao` / `cmpdd` 是设计依据,**不是事实来源**;引用其结论时必须在本项目 - 重新验证。 +- 业务事实和 schema 变化同步本文与 [api.md](api.md),代码不得另起一套字段或状态。 +- 第四节安全边界只能收紧。需要变更时先更新架构、任务边界和理由。 +- 页面高风险能力先真机取证并隔离测试,再接入完整流程。 +- 前序项目 `cmroubao` / `cmpdd` 只提供设计理由,不提供可直接复用的页面事实。 diff --git a/docs/05-coding-rules.md b/docs/05-coding-rules.md index 380a658..df74def 100644 --- a/docs/05-coding-rules.md +++ b/docs/05-coding-rules.md @@ -24,15 +24,14 @@ 明确建立、闸门二通过、闸门三通过、控件唯一。 - 围栏接口超时、冲突、网络失败或响应不明时不得点击;围栏建立后不得释放授权、重新领取 或再次点击,只能恢复同一 `order_submission` 并调和结果。 -- 点击后无论超时、跳外部支付还是遇安全校验,**一律转人工、禁止重试**,授权立即标记 - 已消费。 -- **第一趟试选的代码路径不得引用 `go_to_order_confirm()` 与 `submit_order()`**, - 必须有测试证明不可达。 -- 第一趟只可通过独立的 `open_trial_sku_panel()` capability 点击本项目真机证据与 App 版本绑定的 - 精确唯一入口;当前仅允许拼多多 `8.17.0` 上已取证的 `快要抢光`。不得向第一趟暴露通用 `click`、 - `set_quantity()`、订单确认、提交或付款能力,不得把其他购买文案作为包含/同义匹配兜底。 -- 规格面板内即使可见“提交订单”、微信支付、先用后付或 0 元下单,第一趟也只能把它们作为硬拒绝 - 判据,绝不能返回可点击对象或尝试继续。 +- 点击后无论超时、跳外部支付还是遇安全校验,**一律转人工、禁止重试**。授权保持已围栏; + 只有明确结果或人工调和后才记为已消费/已解决。 +- T-103 的 `SkuSelectionFlow` 不得引用数量、订单确认、提交围栏、`submit_order_once()` 或付款能力, + 必须有静态调用链和测试证明不可达。 +- `open_sku_panel()` 只可点击本项目真机证据与 App 版本绑定的精确唯一入口;当前仅允许拼多多 + `8.17.0` 上已取证的 `快要抢光`。不得暴露通用 `click`,不得把其他购买文案按包含/同义/坐标兜底。 +- 规格面板中的“提交订单”、微信支付、先用后付或 0 元下单在 T-103 只能作为硬拒绝判据,绝不能 + 返回可点击对象。后续生产单趟也只能在各段已取证且四条件全通过时触发封装的一次性提交能力。 ### 1.2 匹配纪律 @@ -46,8 +45,8 @@ - 价格**只在规格面板(闸门一 / 二)和订单确认页(闸门三)读**。 - **不从商品详情页正文、搜索结果卡片或任何其他位置读价格。** - 读不到就转人工,**不用别处的数字凑合**。 -- 第二趟重读的单价**必须与授权锁定价一致**,不一致即停——人确认的是那个价格, - 不是那个商品。 +- 同一趟设置并复核数量后,闸门二重读的规格必须不变、单价必须与闸门一完全一致;授权锁定的是 + 最高总价而不是旧观察价。价格变化即停。 - 图片搜索(V2)的唯一产出是 `goods_id`,同样不读价。 ### 1.4 安全与隐私 @@ -97,10 +96,9 @@ - V2 / V3 只记录,不实现。**图搜、Excel、ERP、订单自动核对、AI 辅助全部不在 MVP。** - 需求明确排除的非目标不得实现。 - 不为「将来可能用到」提前抽象。 -- **Phase 1 真机结论出来之前,不写依赖真机可读字段或会启动试选的 Phase 2 功能。** T-201 - 管理会话与只创建 `DRAFT` 的 T-202 基础建单 / 列表可以并行;T-203 的批量开始试选、T-204 - 的试选证据详情以及 T-205 以后仍等待 T-103。T-201 / T-202 不得夹带机器实际规格、规格面板 - 单价、证据、授权、提交或付款字段。 +- **Phase 1 真机结论出来之前,不写依赖真机可读字段的生产执行功能。** T-201 管理会话、只创建 + `DRAFT` 的 T-202,以及 T-203 只锁定现有任务字段的服务端开始采购授权事务可以并行;T-205 起 + 的实际 attempt / 页面证据等待 T-103。T-201 / T-202 不得夹带授权、机器结果、提交或付款字段。 ## 5. 架构纪律 diff --git a/docs/06-tasks.md b/docs/06-tasks.md index 2347a69..0e86372 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -1,42 +1,37 @@ # 任务路线图(Roadmap) -> 本文是**只读路线图**:维护阶段划分、里程碑、待办池和建议拆分清单。 -> 真实任务以「一任务一文件」存放在 [`tasks/`](tasks/README.md)(`docs/tasks/T-<编号>.md`), -> 状态权威在任务文件 frontmatter。**本文不跟踪单任务状态。** +> 本文维护阶段、依赖和建议拆分,不跟踪状态。真实状态、写路径和执行证据在 +> [`docs/tasks/T-XXX.md`](tasks/README.md);状态权威是 git frontmatter。 ## 使用规则 -1. **开工先落文件**:从下方清单把下一个任务落成 `docs/tasks/T-<编号>.md`(沿用建议编号), - 把验收要点展开成可执行、可观察的步骤,再开始实现。 -2. **每个 agent 一次只做一个任务**:领取、状态流转、执行记录、完成定义遵循 - [`tasks/README.md`](tasks/README.md) 和[编码规则](05-coding-rules.md)。 -3. **不跳步**:依赖未完成的任务不能开工。T-103 完成前可并行 T-201 管理会话与只创建 `DRAFT` - 的 T-202 基础建单 / 列表;**依赖真机可读字段或会启动试选的 Phase 2 功能仍不得抢跑**, - T-103 若改变可读字段则先修订原型与 IX。 -4. **本文只在规划变化时修改**:单个任务开工或完成**不**修改本文。 -5. **动手前**先读 `00-ai-start-here.md`、`05-coding-rules.md` 和 `current-state.md`。 +1. 从路线图领取任务前先落任务文件、边界、写路径和验收门禁。 +2. 一个 agent 同时只负责一个 `DOING`;只有写路径互不重叠才并行。 +3. `needs_device: true` 只能由人完成验收,agent 写完代码仍保持 `DOING`。 +4. 页面判据必须先在本项目真机取证。真机结论变化时先修订契约/原型,再写生产流程。 +5. 管理员点击“开始采购”是创建待付款订单的授权;单趟业务流程不等于跳过分段取证。 ## 并行执行与关键路径 -执行顺序以任务表中的 `依赖` 为准,Phase 是里程碑分组,**不是要求整段串行的栅栏**。 -在 `write_paths` 不重叠且依赖已完成的前提下,按以下波次推进: +Phase 是里程碑分组,不是整段串行栅栏。推荐波次: -| 波次 | 采购服务方向 | 采购工具 / 真机方向 | 集成方向 | +| 波次 | 采购服务 agent | 采购工具 agent | 大脑 / 集成 | | --- | --- | --- | --- | -| 1 | T-001 初始化 `admin/` | T-002 初始化 `client/` | - | -| 2 | T-001 后立即做 T-004 | T-002 后立即做 T-101 → T-102 → **T-103** | T-001、T-002 都完成后做 T-003 | -| 3 | 与 T-103 并行做 T-201 → T-202;T-103 后做 T-203 → T-207 | T-103 后推进 T-104 → T-107 | T-208 等待 T-207 与 T-107 均完成 | -| 4 | T-301 → T-302 | T-302 后交接 T-303 → T-304 → T-306 | T-306 与 T-104 完成后做 T-305 | -| 5 | - | T-305、T-208、T-306 后做 T-401 | T-401 后并行 T-402 / T-403,再做 T-404 → T-405 | +| 1 | T-001 → T-004 → T-201 → T-202 | T-002 → T-101 → T-102 | T-003、原型与契约 | +| 2 | T-209 → T-203 → T-204 | T-103 → T-104 → T-105 → T-107 | T-111 单趟重构;逐任务主审 | +| 3 | T-205 → T-207 → T-208 | T-301 → T-304 → T-306 | 双端契约完整门禁 | +| 4 | - | T-305 | 围栏前端到端 dry-run 主审 | +| 5 | T-402 / T-403 | T-401 | 真机人工授权、提交与调和验收 | +| 6 | - | T-405 | T-404 完整验收 | -**T-103 是当前最高优先级和 MVP 生死线。** T-002 一完成就启动 T-101,不等待 T-003、 -T-004 或整个 Phase 0 收尾。并行只优化等待关系,不改变下列门禁: +并行门禁: -- T-103 的真机结论出来前,T-201 / T-202 只能落管理会话、`DRAFT` 建单与基础列表;不启动试选, - 不展示机器规格、规格面板单价或证据。T-203~T-207 继续等待 T-103。 -- `needs_device: true` 的任务仍只能由人完成验收。 -- 不复用前序项目页面判据,不放宽三道价格闸门,不让第一趟引用任何下单函数。 -- 第三个 agent 优先做写路径独立的集成任务或只读复核,不与两端任务争写共享文档。 +- T-202 只创建/展示 `DRAFT`,可与 T-103 并行;T-203 只实现管理员授权与任务状态,不依赖页面 + 选择器,也可在 T-103 完成前推进。 +- T-205 起涉及实际 attempt/证据字段,必须等待 T-103 确认规格和价格最小事实。 +- T-103、T-105、T-106、T-107 的 capability 相互隔离;未经取证的后续动作不可提前暴露。 +- T-401 前任何任务都不得真实点击“提交订单”;支付能力永远不实现。 +- 主 agent 独立审阅每个子 agent 的 diff 并重跑验证,不以其自报完成作为证据。 ## 建议拆分清单 @@ -44,101 +39,100 @@ T-004 或整个 Phase 0 收尾。并行只优化等待关系,不改变下列 | ID | 任务 | 依赖 | 验收要点 | | --- | --- | --- | --- | -| T-001 | 初始化采购服务 `admin/` Go 骨架 | - | 在 `admin/` 创建 Go 模块;`go test ./...`、`go vet ./...` 可运行;健康检查端点可访问;用真实命令替换 `03-tech-stack.md`、`00-ai-start-here.md`、`current-state.md` 中的占位命令 | -| T-002 | 初始化采购工具 `client/` Python 骨架 | - | 在 `client/` 创建虚拟环境、`requirements.txt` 与应用骨架;`unittest`、`python -m compileall` 可运行;日志与产物目录策略明确且不记录敏感信息 | -| 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-007 | 统一产品名称与源码目录契约 | - | 项目名固定为 `cmbuyer`;采购服务使用 `admin/`,采购工具使用 `client/`;架构、任务和启动脚本不再把 `web/` / `desk/` 当生产源码目录 | +| T-001 | 初始化采购服务 `admin/` | - | Go 模块、健康检查、`go test` / `go vet` 可运行 | +| T-002 | 初始化采购工具 `client/` | - | Python 包、unittest、compileall、日志/产物目录可运行 | +| T-003 | 建立 `init.ps1` 统一入口 | T-001, T-002 | 一条命令安装并验证两端;失败明确,不静默跳过 | +| T-004 | 建立核心数据模型与状态机 | T-001 | 基础 tasks / authorization / submission 模型、十进制金额和状态测试 | +| T-005 | 网页端 MVP 原型 | - | 采购服务登录、传统表格、创建、批量开始采购、路由详情、待付款/调和状态可演示 | +| T-006 | 桌面端 MVP 原型 | - | 采购工具固定 tabs、轮询、当前任务/图片/日志、记录详情双视图和单趟状态可演示 | +| T-007 | 统一产品与目录名称 | - | 项目 `cmbuyer`;网页端“采购服务”在 `admin/`;桌面端“采购工具”在 `client/` | +| T-010 | 原型与交互核对 | T-005, T-006 | 人工确认 P0 页面、布局、键盘、异常和无生产副作用 | -### Phase 1 · 真机取证(最高风险,必须先做) +### Phase 1 · 真机取证(最高风险) -> 本阶段每个任务都需要真机,**只能由人完成验收**。结论写入任务文件并同步 -> `04-architecture.md`;**每条页面判据必须记录拼多多 App 版本**。 +> 每个任务均 `needs_device: true`。截图、XML、goods_id、设备、Android、连接方式和拼多多 App +> 版本写入任务记录;只有人能把任务标为 DONE。 | ID | 任务 | 依赖 | 验收要点 | | --- | --- | --- | --- | -| T-101 | 验证 ADB 与 uiautomator2 连接(USB + WiFi) | T-002 | 两种连接都能列设备、截图、`dump_hierarchy(compressed=False)`;同一手机双通道在线时报明确错误;超时参数可配置 | -| T-102 | 验证按链接打开商品详情页 | T-101 | 输入 `goods_id` 链接后真机进入对应详情页;打不开时有可区分的失败原因;保存截图与页面 XML | -| T-103 | 验证规格面板打开、按维度精确选择、**读取 SKU 单价** | T-102 | 能打开规格面板;按 `颜色分类=X`、`尺码=Y` 精确选中并读回确认;**防前缀碰撞**;找不到精确值时停止且不点相近项;**取证单价在哪个节点、是否带券后前缀**——闸门一依赖此结论 | -| T-104 | 验证试选后安全退出并释放手机 | T-103 | 读完价截完图后退出商品页;**全程不点击「现在买」或任何下单入口**,有测试证明;退出后可立即开始下一条任务 | -| T-105 | 验证数量设置与复核 | T-103 | 设置后读回精确等于要求值;不等时停止,不进入购买入口 | -| T-106 | dry-run:验证进入订单确认页并读「实付款」 | T-105 | 进入确认页读出规格、数量、实付金额;验证提交控件唯一但**绝不点击**;不提取地址原文与手机号;保存截图与 XML 证据到任务产物 | -| T-107 | 固化提交控件判据与 dry-run 安全边界 | T-106 | 只把本项目真机证据转成可测试判据;第一趟和 dry-run 路径不可达 `submit_order`;App 版本不匹配时 fail closed;**不创建真实订单** | +| T-101 | 验证 ADB / uiautomator2(USB + WiFi) | T-002 | 两通道截图和 dump;同手机双通道 fail closed;超时可配置 | +| T-102 | 验证按链接打开商品详情页 | T-101 | canonical goods 链接进入对应商品;前台包与 goods_id 有证据 | +| T-103 | 验证受控规格面板、精确选择与 SKU 单价 | T-102, T-110, T-111 | 精确唯一入口、维度内等值选择/读回、防前缀碰撞、当前价唯一读取;隔离 capability 不含数量/确认/提交 | +| T-104 | 验证规格选择能力安全退出 | T-103 | 完成选择/读价/截图后可关闭面板或退出;不进入确认页、不提交 | +| T-105 | 验证数量设置、读回与闸门二 | T-103 | 设置数量并精确读回;再次读规格/单价等于闸门一;不一致即停 | +| T-106 | dry-run:确认页与闸门三取证 | T-105 | 进入确认页读规格、数量、应付总额并验证提交控件唯一;绝不点击 | +| T-107 | 固化确认页与提交控件安全判据 | T-106 | 只使用本项目证据;dry-run 静态不可达提交;版本失配 fail closed | +| T-110 | 批准证据绑定的受控规格入口 | T-103 no-go 决策 | 仅批准拼多多 8.17.0 已取证的精确唯一“快要抢光”,不泛化 | +| T-111 | 调整为开始采购授权的单趟流程 | T-004, T-010, T-110 | 状态/API/原型/任务依赖一致;删除试选后确认但不放宽三闸门和围栏 | -> **T-106 与 T-107 只做只读演练,不得点击提交订单。** 本路线图中首次允许创建真实订单 -> 的任务是 T-401;执行前必须取得明确授权,并在任务文件记录订单是否产生、如何处置。 +T-106 / T-107 是发布前只读验证,不是业务任务的第一趟。首次允许创建真实待付款订单的是 T-401, +执行前必须有明确人工授权,并记录订单是否产生及如何处置。 ### Phase 2 · 采购服务核心 | ID | 任务 | 依赖 | 验收要点 | | --- | --- | --- | --- | -| T-201 | 管理员登录与会话 | T-004, T-005 | 登录建立会话;CSRF 生效;未登录跳转并只接受站内返回路径 | -| T-202 | 手工建单(F-001) | T-201 | 列表内创建弹窗与 `/tasks/new` 复用表单;任务名称、链接、颜色分类、尺码、数量、价格上限校验;成功创建 `DRAFT` 并倒序显示第一行 | -| T-203 | 任务表格、查询与批量开始试选(F-004、F-018) | T-202 | 传统表格无操作列;标题关键词筛选;只选 `DRAFT`;批量 `DRAFT → PENDING` 全有或全无;空状态可清除筛选 | -| T-204 | 路由化任务详情(F-011) | T-203 | 从列表进入显示保留现场的右侧抽屉,直达显示完整页;按状态呈现唯一主区块与主动作;执行证据可查看 | -| T-205 | 试选结果接收与确认页(F-007) | T-204, T-103 | 接收 `spec_trials` 与规格面板截图;确认页展示需求 vs 所选、单价、合计、截图;**轻量版,非对照台** | -| T-206 | 人工确认与授权签发(F-008) | T-205 | 确认即签发一次性授权并**锁定试选单价**;`expected_task_version` 冲突返回 409;金额上限服务端校验;支持「退回,不买」 | -| T-207 | 授权超时与围栏前放弃(F-010) | T-206 | 围栏前超时 / 放弃后任务转 `PENDING_RETRIAL`,必须重新试选;围栏后禁止超时释放或放弃,改走人工核查;每个状态都有安全下一步 | -| T-208 | 提交围栏与结果调和 API(F-017) | T-207, T-107 | dry-run start / ready、submission start / reconcile / manual-review 幂等;围栏事务原子消费执行权;响应不明不允许点击;不确定结果保留额度且不可重试 | +| T-201 | 管理员登录与会话 | T-004, T-005 | 会话、CSRF、站内 return path 与权限测试 | +| T-202 | 手工建单(F-001) | T-201 | 创建/展示 `MANUAL + DRAFT`;链接/金额/幂等/倒序;不含授权或执行 | +| T-209 | 把核心 schema / 状态机迁移为单趟模型 | T-004, T-111 | 新 migration 安全替换旧 `spec_trials` / 授权锁价结构;新增 `purchase_attempts`;删除旧 MVP 状态;无业务数据事实写入迁移前置,遇未知旧数据 fail closed;领域/迁移测试同步 | +| T-203 | 表格查询与批量开始采购(F-004、F-008、F-018) | T-202, T-209 | 只选 DRAFT;按钮明确“不付款”;事务内创建锁定任务字段的一次性授权并全批 `DRAFT → PENDING`;重放/冲突测试 | +| T-204 | 路由化任务详情与内部截图资产(F-007、F-011) | T-203 | 抽屉/完整页共享 URL;状态详情;受保护 `INTERNAL_RAW` screenshot 上传/读取、SHA-256 与 no-store;不接收 XML | +| T-205 | purchase attempt、事件与执行摘要 API | T-204, T-103 | 记录实际规格、两次单价、数量、确认页金额、固定 reason;不再创建 `spec_trials` 审批记录 | +| T-206 | 单趟状态视图与安全主动作 | T-205 | PENDING/CLAIMED/ORDERING/NEEDS_MANUAL/WAITING_PAYMENT/RECONCILIATION_REQUIRED 显示和允许动作一致;无中途确认 | +| T-207 | 授权有效期与围栏前重置(F-010) | T-206 | 围栏前过期/关闭旧授权后回 DRAFT;围栏后拒绝释放、取消、重新授权 | +| T-208 | 提交围栏与结果调和 API(F-017) | T-207, T-107 | 原子复核授权/attempt/三闸门/唯一控件;首次许可一次;重放不许可点击;结果不明永久围栏且只调和 | -### Phase 3 · 双端打通(第一趟) +### Phase 3 · 双端打通(单趟围栏前) | ID | 任务 | 依赖 | 验收要点 | | --- | --- | --- | --- | -| T-301 | 设备凭据与身份隔离(F-013) | T-201 | 设备 Bearer 不能建单或授权;管理会话不能调设备接口;凭据可撤销 | -| T-302 | 原子领取与租约(F-005) | T-301, T-004 | 并发领取只有一个成功;重复领取重放同一结果;**同时支持领取待试选与已授权两类**;`claim_token` 与 `claim_generation` 校验生效 | -| T-303 | 采购工具 `HttpTaskSource` / `HttpResultSink` | T-302, T-002 | 执行器只依赖抽象;测试假数据与断连 JSONL 暂存不扩大 Excel MVP 范围;补传使用幂等键 | -| T-304 | 定时轮询与会话边界 | T-303, T-006 | 只在执行员启动的会话内轮询,关窗口即停;连续失败达阈值自动停止并提示原因;间隔可配置 | -| T-306 | 证据上传与分层 | T-304, T-103 | 规格面板截图与订单截图上传服务端并脱敏;节点树与失败现场留本地;上传前校验不含地址与手机号 | -| T-305 | **第一趟试选端到端** | T-306, T-104 | 从领取跑到试选回传:开商品、勾选规格、读单价、截图、退出释放手机;任务转「等你确认」;全程有事件与证据 | +| T-301 | 设备凭据与身份隔离(F-013) | T-201 | Bearer 不能建单/授权;管理会话不能领任务;凭据可撤销 | +| T-302 | 已授权任务原子领取与租约(F-005) | T-301, T-203 | 只领 PENDING+有效授权;并发唯一;重放同一 attempt;claim token/generation 有效 | +| T-303 | `HttpTaskSource` / `HttpResultSink` | T-302, T-002 | 执行器依赖抽象;幂等重放;线协议严格校验;不扩大 Excel 范围 | +| T-304 | 定时轮询与会话边界 | T-303, T-006 | 人启动后轮询;停止只阻止下次领取;连续失败停;当前任务/记录详情双视图 | +| T-306 | 单趟截图与证据上传 | T-304, T-204 | 显式上传原始 screenshot;可含页面地址/手机号;完整 XML、本机路径、外部支付页不上传 | +| T-305 | **单趟围栏前端到端 dry-run** | T-306, T-107, T-205 | 从领取连续跑到闸门三与提交控件验证,随后安全停止;全程事件/证据齐全,不申请真实许可、不点击提交 | -### Phase 4 · 第二趟与收尾 +### Phase 4 · 真实提交与收尾 | ID | 任务 | 依赖 | 验收要点 | | --- | --- | --- | --- | -| T-401 | **第二趟下单端到端** | T-305, T-208, T-306 | 拿到授权后重新开商品、重新选同一规格、过三道闸门、原子建立提交围栏后提交一次;不确定结果进入调和且不可重试;明确创建才转「待付款」 | -| T-402 | 待付款收口与手工完成 | T-401 | 详情页展示订单截图与授权信息供核对;人付款后手工标记完成;**待付款不等于成功** | -| T-403 | 失败分类与证据归档(F-011) | T-401 | 覆盖需求列出的全部失败原因;终态一次原子回写,不落中间态 | -| T-404 | 完整验收 MVP | T-402, T-403 | `02-requirements.md` 的 P0 验收全部通过;真机记录写入任务文件与 `current-state.md` | -| T-405 | 采购工具打包与运行文档 | T-404 | 运营电脑可按文档运行;记录产物 SHA-256 | +| T-401 | **单趟采购与提交围栏端到端** | T-305, T-208 | 同一设备会话通过三闸门、首次明确围栏许可后点击一次;结果不明只调和;明确创建转待付款;绝不付款 | +| T-402 | 待付款收口与手工完成 | T-401 | 详情展示订单证据;人付款后手工标记;待付款不等于成功 | +| T-403 | 失败分类与证据归档 | T-401 | 覆盖需求 reason;终态原子回写;围栏后无重试 | +| T-404 | 完整 MVP 验收 | T-402, T-403 | P0 验收、双端门禁、真机记录和 current-state 全部通过 | +| T-405 | 采购工具打包与运行文档 | T-404 | 运营电脑可安装运行,记录产物 SHA-256 | -### V2 及以后(不在 MVP,编号预留) +### V2 及以后 | ID | 任务 | 说明 | | --- | --- | --- | -| T-501 | 图片搜索产出 goods_id(F-014) | 推图到相册、搜图、从结果页取 goods_id;**不读价格**;最多 5 个 | -| T-502 | 候选对照台(F-015) | 多候选并排对照,跨列逐行对齐,窄屏降级 | -| T-503 | Excel 批量建单(F-002) | 被表头契约待确认阻塞 | -| T-504 | ERP 货运同步与建单(F-003) | 被字段映射待确认阻塞 | -| T-505 | 批量顺序编排与人工接管(F-012) | 勾选多条、暂停继续、运行中冻结 | -| T-506 | 订单自动核对回读(F-016) | 五项唯一匹配才自动回写,否则待人工 | -| T-507 | 本地执行轨迹留档(F-103) | NDJSON,含规则与模型判断的分歧字段 | -| T-508 | AI 辅助(F-101、F-102) | 模型结论**不能放宽任何安全边界** | +| T-501 | 图片搜索产出 goods_id(F-014) | 不在结果页读价;最多 5 个 | +| T-502 | 候选对照台(F-015) | 多候选对照,窄屏降级 | +| T-503 | Excel 批量建单(F-002) | 等表头契约 | +| T-504 | ERP 同步与建单(F-003) | 等字段映射 | +| T-505 | 批量编排与运行中接管(F-012) | 排序、暂停继续、运行中冻结 | +| T-506 | 订单自动核对回读(F-016) | 五项唯一匹配,否则人工 | +| T-507 | 本地执行轨迹留档(F-103) | NDJSON 与模型/规则分歧 | +| T-508 | AI 辅助(F-101、F-102) | 模型结论不能放宽安全边界 | ## 里程碑 -- **M0**:网页端与桌面端 P0 原型经人工确认,流程、状态与主动作可枚举。(T-005、T-006) -- **M1**:两端骨架可运行,数据模型与状态机落地。(Phase 0) -- **M2**:真机能按链接打开商品、精确勾选颜色分类和尺码、**读到该 SKU 单价**。(T-103) -- **M3**:真机能设对数量、以 dry-run 进入订单确认页读到「实付款」并验证唯一提交控件, - 但不点击。(T-107) -- **M4**:管理员能建单、看到试选结果、确认并签发授权。(Phase 2) -- **M5**:第一趟试选端到端跑通,任务能停在「等你确认」。(T-305) -- **M6**:MVP 闭环——第二趟下单成功,任务停在「待付款」。(T-401) +- **M0**:两端原型确认;单趟主动作、状态、异常和键盘可枚举。(T-005、T-006、T-111) +- **M1**:两端骨架、基础模型与验证入口可运行。 +- **M2**:真机按链接打开商品、精确选择颜色/尺码并读 SKU 单价。(T-103) +- **M3**:真机设置数量、闸门二、确认页闸门三和唯一提交控件 dry-run 通过,不点击。(T-107) +- **M4**:管理员建单并通过批量开始采购创建一次性授权;设备可安全领取。(T-203、T-302) +- **M5**:单趟从领取到围栏前 dry-run 跑通,事件与证据齐全。(T-305) +- **M6**:单趟真实提交只创建待付款订单,围栏/调和/人工收口闭环。(T-401~T-404) -**M2 是本项目的生死线。** 前序项目正是卡在选规格和读价;M2 不通过之前只允许 T-201 与 -不启动试选的 T-202,不写依赖真机字段或会推进任务执行的 Phase 2 功能。原型只用于确认信息架构, -T-103 若改变可读字段必须先回修原型与 IX。 +**M2 仍是生死线。** M2 前可以并行管理会话、DRAFT 建单和开始采购的服务端授权事务,但不能编写 +依赖未取证页面字段的生产执行逻辑。提速来自两端并行、窄任务和主审复用门禁,不来自删除安全闸门。 -## 待办池(Backlog) +## 待办池 -- V2 全部条目见上方「V2 及以后」表(T-501 ~ T-508) -- 多设备并行(F-104) -- 完整 RBAC(F-105) -- 多平台比价(F-106) -- 证据保留期与自动清理策略 -- 采购服务从 SQLite 迁移到 Postgres 的评估 -- 设备凭据轮换机制 -- 定时轮询间隔的风控友好节奏(需真机观察后确定) +- V2:T-501~T-508;多设备、完整 RBAC、多平台。 +- 证据保留期和自动清理策略。 +- SQLite → Postgres 迁移评估。 +- 设备凭据轮换。 +- 风控友好的轮询与动作间隔(需真机观察)。 diff --git a/docs/07-user-stories.md b/docs/07-user-stories.md index 7f766a2..8997ae6 100644 --- a/docs/07-user-stories.md +++ b/docs/07-user-stories.md @@ -1,259 +1,139 @@ # 用户故事清单 -> 本文记录「谁在什么场景下,为了获得什么价值,要完成什么目标」。不写接口、数据字段、 -> 组件实现或逐个按钮的行为。 -> 页面如何响应操作见[交互清单](08-interaction-checklist.md);页面入口见[路由与页面结构](routes.md); -> 接口形状以 [API 合约](api.md) 为准。 +> 本文从用户目标描述 MVP。功能编号以[需求](02-requirements.md)为准,具体界面行为以 +> [交互清单](08-interaction-checklist.md)为准。 ## 一、职责边界 -| 信息 | 权威文档 | -| --- | --- | -| MVP 范围、优先级、非目标 | [需求](02-requirements.md) | -| 用户目标、场景与验收场景 | 本文 | -| 页面操作、状态与反馈 | [交互清单](08-interaction-checklist.md) | -| 页面入口、导航与组件归属 | [路由与页面结构](routes.md) | -| 接口、事件与错误格式 | [API 合约](api.md) | - -US 编号一经引用不再重用。需求变化时先改[需求](02-requirements.md),再同步本文。 +- 采购管理员决定买什么、最高允许多少钱,并通过“开始采购”明确授权创建待付款订单。 +- 采购工具只执行已授权任务,不修改规格、数量或金额上限。 +- 系统不付款;待付款订单由人核对、付款并手工收口。 +- 截图是内部审计证据,不是提交前审批环节。 ## 二、用户故事总表 -| ID | 标题 | 优先级 | 角色 | 要达成的目标 | 关联功能 | 关联交互 | 状态 | +| ID | 故事 | 优先级 | 角色 | 用户价值 | 功能 | 交互 | 状态 | | --- | --- | --- | --- | --- | --- | --- | --- | -| US-001 | 把「要买什么」变成一条任务 | P0 | 采购管理员 | 填一次链接和规格,后面不用再管 | F-001 | IX-002 | 已定 | -| US-002 | 知道每条任务现在卡在谁那里 | P0 | 采购管理员 | 一眼分出「等我」和「机器在跑」,快速找到要处理的那条 | F-004、F-011 | IX-003、IX-004 | 已定 | -| US-003 | 让手机自己跑,不用我盯着 | P0 | 采购执行员 | 开一次轮询,手机排着队跑,只在停下来时介入 | F-005、F-006 | IX-007、IX-008 | 已定 | -| US-004 | 确认机器有没有选对 | P0 | 采购管理员 | 看一眼机器实际勾选到的规格和价格,判断能不能买 | F-007、F-008 | IX-005 | 已定 | -| US-005 | 放心地授权一笔下单 | P0 | 采购管理员 | 授权后确信系统只下单不付款,且不会因断网重复下单 | F-008、F-009、F-010、F-017 | IX-005、IX-010、IX-011 | 已定 | -| US-006 | 付款前核对订单再花钱 | P0 | 采购管理员 | 对着系统给的几项组织人工核对,不一致就不付 | F-009 收口 | IX-006 | 已定 | -| US-007 | 管理身份和设备身份互不越权 | P0 | 采购管理员 | 设备被盗用也不能建单或改授权 | F-013 | IX-001 | 已定 | -| US-008 | 任务出问题时知道该怎么办 | P0 | 采购执行员 | 看到具体原因和下一步,而不是一个「失败」 | F-011 | IX-008 | 已定 | -| US-010 | 把选中的任务交给机器试选 | P0 | 采购管理员 | 一次选中多条待开始任务,让桌面端逐条领取第一趟试选 | F-018 | IX-012 | 已定 | - -> US-009(从 ERP 货运明细建单)随 F-003 移出 MVP,编号保留不重用。 +| US-001 | 把“要买什么”变成待开始任务 | P0 | 采购管理员 | 一次填写链接、规格、数量和最高总价 | F-001 | IX-002 | 已定 | +| US-002 | 知道任务当前卡在哪里 | P0 | 采购管理员 | 快速找到待授权、执行中、待付款或待人工任务 | F-004、F-007、F-011 | IX-003、IX-004 | 已定 | +| US-003 | 让手机逐条执行已授权任务 | P0 | 采购执行员 | 开启轮询后无需盯每一步,异常时再介入 | F-005、F-006 | IX-007、IX-008 | 已定 | +| US-004 | 查看机器实际执行证据 | P0 | 采购管理员 | 追溯规格、价格、数量、围栏和截图 | F-007 | IX-004 | 已定 | +| US-005 | 明确授权且不会重复下单 | P0 | 采购管理员 | 点击开始采购后,只创建一笔待付款订单且绝不自动付款 | F-008、F-010、F-017 | IX-005、IX-010、IX-011 | 已定 | +| US-006 | 付款前核对订单 | P0 | 采购管理员 | 对照系统证据后由人决定是否付款 | F-006 收口 | IX-006 | 已定 | +| US-007 | 管理身份与设备身份不越权 | P0 | 采购管理员 | 设备凭据泄露也不能建单或授权 | F-013 | IX-001 | 已定 | +| US-008 | 出问题时知道下一步 | P0 | 采购执行员 | 看到具体阶段、原因和安全恢复方式 | F-011、F-017 | IX-008、IX-011 | 已定 | +| US-010 | 批量授权待开始任务 | P0 | 采购管理员 | 一次把选中任务交给采购工具逐条创建待付款订单 | F-008、F-018 | IX-012 | 已定 | ## 三、故事详情 -### US-001 把「要买什么」变成一条任务 +### US-001 把“要买什么”变成待开始任务 -- 优先级:P0 | 关联功能:F-001 | 关联交互:IX-002 -- 角色:采购管理员 -- 前置条件:已登录,手上有拼多多商品链接和要买的颜色分类、尺码。 +作为采购管理员,我希望在任务列表点击创建,填写标题、拼多多链接、颜色、尺码、数量和最高总价, +保存后立即在首行看到 `DRAFT`,以便稍后统一授权。 -**用户故事** +验收场景: -作为采购管理员,我想要填一次链接和规格就把任务交出去,从而不必自己再去手机上操作。 +- 合法输入保存成功并规范化商品链接;创建动作不触发设备和授权。 +- 链接、规格、数量或金额不合法时,在字段附近说明原因并保留其他输入。 +- 重复提交同一 `create_key` 只返回原任务,不创建重复记录。 -**范围** +### US-002 知道任务当前卡在哪里 -- 包含:任务名称、手工填链接、颜色分类、尺码、数量、价格上限。 -- 不包含:Excel 批量导入、从 ERP 生成、任务模板(均为 V2)。 +作为采购管理员,我希望在传统表格中按标题和状态筛选,看到标题链接、颜色、尺码、价格、数量、 +采购结果、状态和创建时间,并双击行查看详情。 -**验收场景** +验收场景: -1. 假如我有商品链接和规格,当我在任务列表的创建弹窗填完并保存,那么得到任务编号, - 弹窗关闭,任务按创建时间倒序出现在表格第一行且状态为待开始。 -2. 假如链接无法解析出商品标识,当我提交,那么系统明确报错并保留我已填的内容。 -3. 假如我没填价格上限,当我提交,那么系统拒绝——**价格上限是资金边界,不能留空**。 +- 默认按创建时间倒序;标题是指向拼多多商品页的链接。 +- 没有操作列;详情通过双击行打开路由化抽屉,完整 URL 可复制,浏览器返回恢复列表现场。 +- 状态明确区分待开始、已授权待领取、执行中、待人工、围栏后待调和、待付款和终态。 +- 空筛选结果提供“清除筛选”,错误页保留安全重试;不把已围栏提交显示为可重试。 -### US-002 知道每条任务现在卡在谁那里 +### US-003 让手机逐条执行已授权任务 -- 优先级:P0 | 关联功能:F-004、F-011 | 关联交互:IX-003、IX-004 -- 角色:采购管理员 -- 前置条件:已登录,系统中有若干条不同状态的任务。 +作为采购执行员,我希望配置服务、设备 serial 和轮询间隔后,点击一次开始轮询,让采购工具逐条领取 +已授权任务并在同一趟中完成选规格、价格校验、数量、确认页和提交围栏。 -**用户故事** +验收场景: -作为采购管理员,我想要打开就看清哪几条在等我,从而不必逐条点进去确认还需不需要我 -处理。 +- 启动前同时校验采购服务、ADB 设备和已取证拼多多版本;任一不满足则不启动。 +- 顶部始终显示会话状态;当前任务、图片、滚动日志和倒序采购记录实时更新。 +- 精确规格、两次单价、数量读回、确认页总额或提交控件任一不合格时停止该任务。 +- 围栏明确成功后只点一次“提交订单”;系统绝不点击付款。 +- 连续轮询失败达到阈值自动停止;停止轮询不取消正在执行或已围栏的任务。 -**范围** +### US-004 查看机器实际执行证据 -- 包含:传统任务表格、关键词与时间范围查询、任务详情抽屉和完整详情页中的结果与证据。 -- 不包含:自定义视图、导出报表、跨任务统计。 +作为采购管理员,我希望打开任务详情后看到本次授权、设备领取、实际规格、三道价格闸门、截图、 +提交围栏与结果,以便审计发生了什么,而不是在中途批准机器选择。 -**验收场景** +验收场景: -1. 假如有任务停在等待人工确认,当我打开工作台,那么表格状态与采购结果能让我一眼识别, - 双击该行即可在不丢失列表现场的情况下查看详情。 -2. 假如我按关键词筛选后没有匹配,当结果为空,那么显示空状态并提供清除筛选,而不是 - 一片空白或报错。 -3. 假如任务失败了,当我打开详情,那么我看到可区分的失败原因和当时的截图,而不只是 - 「失败」两个字。 +- 证据按 attempt 和时间排序,区分规格面板、确认页、提交结果。 +- 原始截图仅登录管理员可看,响应不缓存;完整 XML 不出现在页面或接口中。 +- 地址/手机号不提取为字段或日志,外部支付页和支付凭据不存储。 +- 执行中没有“确认机器选对了吗”或“确认后继续第二趟”按钮。 -### US-003 让手机自己跑,不用我盯着 +### US-005 明确授权且不会重复下单 -- 优先级:P0 | 关联功能:F-005、F-006 | 关联交互:IX-007、IX-008 -- 角色:采购执行员 -- 前置条件:电脑已连上 web 端;手机已连接、已解锁、已登录拼多多。 +作为采购管理员,我希望点击“开始采购(只创建待付款订单)”时明确知道影响范围,并确信系统不会 +超出金额上限、不会付款,也不会因为断网重复提交。 -**用户故事** +验收场景: -作为采购执行员,我想要开一次轮询就让手机排着队把任务跑掉,从而把注意力留给真正需要 -判断的时刻。 +- 只有 `DRAFT` 可选;操作条显示选中数量和最高总额。 +- 点击即创建一次性授权并整批转 `PENDING`;不再要求试选后确认。 +- 任一任务版本冲突时整批不变;同一开始键重放同一结果。 +- 设备在同一趟通过三道闸门;服务端围栏明确建立后才准许点击一次。 +- 围栏前失败可由人重置并重新授权;围栏后只能调和同一提交,永不出现“再次下单”。 -**范围** +### US-006 付款前核对订单 -- 包含:定时轮询领取、第一趟试选、第二趟下单、连续失败自动停。 -- 不包含:多台手机并行、无人值守整夜运行、已开始任务的排序、暂停 / 继续和运行中接管(V2)。 +作为采购管理员,我希望任务转为待付款后看到商品、规格、数量、授权上限、确认页金额与订单截图, +在拼多多核对后由人决定是否付款。 -**验收场景** +验收场景: -1. 假如有待处理任务且设备就绪,当我点击开始轮询,那么系统按间隔领取任务并显示**这一趟 - 是试选还是下单**;两个实例并发不会领到同一条。 -2. 假如第一趟试选完成,当机器读完价截完图,那么**它退出商品页释放手机**,立刻可以开始 - 下一条,而不是停在规格面板上等我。 -3. 假如某条任务需要人工,当它停下来,那么界面显著提示缺什么,该任务不再被本端领取, - 但轮询继续跑其他任务。 -4. 假如连续失败达到阈值,当失败累积,那么轮询自动停止并说明原因,**不无限重试**。 -5. 假如我关闭窗口,那么轮询立即停止,不留后台进程。 +- `WAITING_PAYMENT` 明确写“订单已创建,系统尚未付款”。 +- 标记完成前再次提示该操作只记录人工付款事实,不执行支付。 +- 人确认已付款后才转 `SUCCEEDED`;发现不一致时不付款并进入人工处理。 -### US-004 确认机器有没有选对 +### US-007 管理身份与设备身份不越权 -- 优先级:P0 | 关联功能:F-007、F-008 | 关联交互:IX-005 -- 角色:采购管理员 -- 前置条件:任务已完成第一趟试选并回传结果。 +作为采购管理员,我希望网页会话与设备 token 完全隔离,以便设备只能做被授权的执行工作。 -**用户故事** +验收场景: -作为采购管理员,我想要看一眼机器**实际勾选到**的规格和读到的价格,从而在它去下单之前 -确认没选错。 +- 未登录不能查看任务或截图;登录会话具备 CSRF 防护。 +- 设备 token 不能创建任务、开始采购、重置任务或人工调和。 +- 管理会话不能伪装设备领取或建立围栏;设备凭据可撤销。 -**范围** +### US-008 出问题时知道下一步 -- 包含:需求与机器所选的逐项对照、单价与合计、规格面板截图、确认或退回。 -- 不包含:在多个候选之间挑选(MVP 只有一个商品,多候选对照台是 V2)。 +作为采购执行员,我希望错误包含阶段、固定原因和安全下一步,以便处理而不冒险重试。 -**验收场景** +验收场景: -1. 假如机器选到的颜色分类和尺码与我要的一致、合计没超上限,当我打开详情,那么各项 - 显示 ✓ 且确认按钮可用。 -2. 假如机器选到的规格与我要的不符,当我打开详情,那么显示 ✗ 并说明哪一项不符, - **确认按钮禁用**,我只能退回或转人工。 -3. 假如合计超出我设的上限,当我打开详情,那么显示超出多少且确认按钮禁用, - **不提供「仍然确认」入口**。 -4. 假如价格读不到,那么任务根本不会进到这一步,而是直接转人工并说明原因。 +- 至少区分设备、App 版本、商品、面板、规格、价格、数量、确认页、围栏、安全校验和网络错误。 +- 围栏前错误提示停止、留证并等待人工处理;不会自动重新开始。 +- 围栏后错误明确写“可能已创建订单,只能调和,禁止再次点击”。 +- 桌面端详情切换不改变服务端状态;Esc 只回到当前任务视图,不取消业务流程。 -### US-005 放心地授权一笔下单 +### US-010 批量授权待开始任务 -- 优先级:P0 | 关联功能:F-008、F-009、F-010、F-017 | 关联交互:IX-005、IX-010、IX-011 -- 角色:采购管理员 -- 前置条件:任务处于等待确认,已看过试选结果。 +作为采购管理员,我希望勾选多条 `DRAFT` 后统一点击开始采购,以便减少重复操作,同时知道每条都 +会允许采购工具创建一笔待付款订单。 -**用户故事** +验收场景: -作为采购管理员,我想要确认后签发一次授权,从而让系统去下单,同时确信它不会替我付钱, -也不会因为卡住而让任务永久停摆。 - -**范围** - -- 包含:确认签发、锁定单价、退回不买、围栏前放弃、dry-run、提交围栏与结果调和。 -- 不包含:审批链、多人会签、金额分级授权。 - -**验收场景** - -1. 假如我提交授权,当系统接受,那么明确告知只创建待付款订单、付款需我在拼多多完成。 -2. 假如授权时的单价是 32.50,当机器第二趟发现价格变了,那么它**停下来转人工**, - 不会按新价照下——我确认的是那个价格,不是那个商品。 -3. 假如一笔授权在提交围栏前卡住,当我需要重来,那么我可以放弃它,任务进入待重新试选。 - 假如围栏已经建立,则不能放弃或重试,只能核查这一次提交。 -4. 假如授权自动过期,那么系统要求**重新试选取新价**后再确认,不复用旧价。 -5. 假如我重复提交同一笔授权,那么只产生一笔订单。 -6. 假如真实点击前网络超时、无法确认服务端是否已建立围栏,那么系统不点击并提示核查; - 假如点击后结果不明,那么系统保留围栏并进入调和,绝不再点一次。 - -### US-006 付款前核对订单再花钱 - -- 优先级:P0 | 关联功能:F-009 收口 | 关联交互:IX-006 -- 角色:采购管理员(采购执行员在拼多多完成人工付款并反馈结果) -- 前置条件:系统已创建待付款订单,任务状态为等待付款。 - -**用户故事** - -作为采购执行员,我想要拿着系统给的几项去拼多多逐一核对,从而不会付错单。 - -**范围** - -- 包含:展示订单截图、商品、规格、数量与授权金额;人付款后手工标记完成。 -- 不包含:系统代付、免密支付、在拼多多改单、**自动回读核对(F-016,V2)**。 - -**验收场景** - -1. 假如订单已创建,当我打开详情,那么我看到需要核对的项和设备回传的订单截图。 -2. 假如各项都对得上,当我在拼多多付完款回来点「已付款」,那么任务转为已完成并记录 - 标记人与时间。 -3. 假如金额或规格对不上,当我发现不一致,那么我能「标记异常」转人工, - **不能直接标记已付款**。 -4. 假如订单截图缺失,那么系统不允许标记完成,直接转人工——**无证据不得收口**。 - -### US-007 管理身份和设备身份互不越权 - -- 优先级:P0 | 关联功能:F-013 | 关联交互:IX-001 -- 角色:采购管理员 -- 前置条件:系统有管理员账号与已授权设备各一。 - -**用户故事** - -作为采购管理员,我想要设备只能做执行、不能做决策,从而即使设备凭据泄露也不会有人 -凭它建单或改授权。 - -**验收场景** - -1. 假如持有设备凭据,当尝试创建任务或签发授权,那么被拒绝并记录。 -2. 假如持有管理会话,当尝试调用设备接口,那么被拒绝。 -3. 假如设备凭据被撤销,当设备下次请求,那么立即失效且当前任务安全停止。 - -### US-008 任务出问题时知道该怎么办 - -- 优先级:P0 | 关联功能:F-011 | 关联交互:IX-008 -- 角色:采购执行员 -- 前置条件:任务执行中遇到异常。 - -**用户故事** - -作为采购执行员,我想要看到具体卡在哪一步、为什么,从而知道是自己能处理还是要找管理员。 - -**验收场景** - -1. 假如规格找不到精确匹配,那么系统说明缺的是颜色分类还是尺码、页面上有哪些可选值, - 并转人工——**不选相近的**。 -2. 假如规格面板上读不到单价,那么系统转人工并说明原因, - **不用商品详情页正文的数字凑合**。 -3. 假如第二趟发现价格与授权价不一致,那么系统说明「授权 ¥X、现价 ¥Y」并转人工, - 既不按新价下单,也不自动放弃。 -4. 假如遇到验证码或风控,那么系统立即停止、保留截图、转人工,**不尝试绕过**。 -5. 假如点击提交订单后无法判断是否已创建,那么系统转人工并提示「订单可能已创建、 - 支付未完成」,进入同一提交记录的调和,**不自动重试、不释放围栏**。 - -### US-010 把选中的任务交给机器试选 - -- 优先级:P0 | 关联功能:F-018 | 关联交互:IX-012 -- 角色:采购管理员 -- 前置条件:已登录;任务表格中至少有一条待开始任务。 - -**用户故事** - -作为采购管理员,我想要勾选一条或多条待开始任务统一开始试选,从而在集中建单后一次交给 -桌面端逐条领取,而不必逐行点击操作按钮。 - -**范围** - -- 包含:当前筛选结果中的可开始任务全选、清除选择、批量进入第一趟试选队列。 -- 不包含:指定执行顺序、暂停 / 继续、运行中接管、多设备调度(F-012,V2)。 - -**验收场景** - -1. 假如我勾选了多条待开始任务,当我点击「开始试选」,那么这些任务一起转为待领取, - 页面明确说明不会创建订单或付款。 -2. 假如任务不是待开始,那么该行不能被本动作勾选,并能看见不可开始的原因。 -3. 假如其中一条任务在提交前已被别人启动,当版本冲突发生,那么整批不产生部分成功, - 页面刷新状态并让我重新选择。 +- 表头复选框只选择当前筛选结果中的 `DRAFT`;切换筛选时选择范围可见且可清空。 +- 未选择时按钮禁用;选中后显示数量、最高总额与“不付款”说明。 +- 点击后批量全有或全无;成功反馈授权数量,冲突反馈刷新重选。 +- 非 `DRAFT` 行不能勾选;表格不提供每行“开始采购”按钮,避免逐行语义不一致。 ## 四、交付前检查 -- [ ] 每个 P0 功能至少关联一个 US 编号。 -- [ ] 每个故事说明角色、目标、价值和可验证的验收场景。 -- [ ] UI 故事已关联对应 IX 编号。 -- [ ] 故事没有复制接口、字段或组件实现细节。 -- [ ] 范围、优先级与[需求](02-requirements.md)一致。 +- 每个 P0 故事至少有一个自动化测试或明确的人机验收证据。 +- 页面、API、状态机只使用单趟采购术语,不再出现试选后确认、授权旧试选价或第二趟下单。 +- 任一可见“开始采购”都同时说明“只创建待付款订单,系统不付款”。 +- 任一围栏后状态都不提供重试、释放授权或重新开始入口。 diff --git a/docs/08-interaction-checklist.md b/docs/08-interaction-checklist.md index 90d3e89..92a6d16 100644 --- a/docs/08-interaction-checklist.md +++ b/docs/08-interaction-checklist.md @@ -1,197 +1,147 @@ # 交互清单 -> 本文把用户故事落成可实现、可测试的界面行为:用户如何触发、系统处于什么状态、如何 -> 反馈,以及失败时怎样恢复。覆盖 web 端页面与 desk 端桌面界面。 +> 本文是界面行为权威。需求范围见[需求](02-requirements.md),页面位置见[routes](routes.md)。 ## 一、职责边界 -| 信息 | 写在哪里 | -| --- | --- | -| 用户目标、价值与业务验收 | [用户故事清单](07-user-stories.md) | -| MVP 范围与优先级 | [需求](02-requirements.md) | -| 页面入口、路由和组件归属 | [路由与页面结构](routes.md) | -| API、事件和错误格式 | [API 合约](api.md) | - -IX 编号一经引用不再重用。交互清单不能扩大需求范围。 +- 采购服务负责建单、开始采购授权、任务详情、内部证据、调和和人工付款收口。 +- 采购工具负责连接手机、轮询领取、单趟执行、日志和本地记录详情。 +- “开始采购”是管理员对创建待付款订单的明确授权;不再有试选后的确认页面。 +- 任一端都不自动付款;围栏后不提供重试或释放动作。 ## 二、交互总表 -| ID | 关联 US | 页面 / 组件 | 触发 | 用户目标 | 预期结果 | 优先级 | 状态 | -| --- | --- | --- | --- | --- | --- | --- | --- | -| IX-001 | US-007 | web `/login` | 提交表单 | 进入管理后台 | 建立会话并进入原目标页或 `/tasks` | P0 | 已定 | -| IX-002 | US-001 | web `/tasks` 创建弹窗;`/tasks/new` 直达兜底 | 提交表单 | 建单 | 解析出 `goods_id`,关闭弹窗并把待开始任务插入表格第一行 | P0 | 已定 | -| IX-003 | US-002 | web `/tasks` 查询条与任务表格 | 输入后筛选 | 找到要处理的任务 | 默认显示标题关键词,可展开状态 / 时间条件;创建时间倒序;空状态可清除 | P0 | 已定 | -| 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-010 | US-005 | web 围栏前放弃授权入口 | 点击放弃 | 解开尚未提交的授权 | 授权作废,任务转待重新试选 | P0 | 已定 | -| IX-011 | US-005、US-008 | web / desk 提交围栏与调和状态 | 真实点击前申请围栏;点击后回报 | 防止断网或重复操作产生第二笔订单 | 围栏前失败不点击;围栏后不明确只核查、不重试 | P0 | 已定 | -| IX-012 | US-010 | web `/tasks` 表格选择与上下文批量栏 | 勾选后点「开始试选」 | 批量交给桌面端试选 | `DRAFT → PENDING` 原子提交;仅开始第一趟,不授权、不下单、不付款 | P0 | 已定 | +| ID | 故事 | 位置 | 触发 | 结果 | 优先级 | 状态 | +| --- | --- | --- | --- | --- | --- | --- | +| IX-001 | US-007 | web `/login` | 登录 / 退出 | 建立或销毁管理员会话 | P0 | 已定 | +| IX-002 | US-001 | web 任务表格创建弹窗 | 点击创建、保存 | 创建 `DRAFT` 并在首行反馈 | P0 | 已定 | +| IX-003 | US-002 | web `/tasks` 筛选行 | 输入关键词、筛选、清除 | 更新表格并保留可理解的筛选状态 | P0 | 已定 | +| IX-004 | US-002、US-004 | web 列表行 / `/tasks/{id}` | 双击行、浏览器前进后退 | 路由化详情抽屉或完整页,显示执行证据 | P0 | 已定 | +| IX-005 | US-005 | web 批量操作条 | 点击开始采购 | 创建一次性授权并进入 `PENDING` | P0 | 已定 | +| IX-006 | US-006 | web 待付款详情 | 查看后手工标记 | 只记录人工付款完成,不执行支付 | P0 | 已定 | +| IX-007 | US-003 | desk 配置 tab | 保存并检查连接 | 服务、ADB、App 版本就绪 | P0 | 已定 | +| IX-008 | US-003、US-008 | desk 采购执行 tab | 开始 / 停止轮询、选择记录、Esc | 单趟执行和安全人工接管 | P0 | 已定 | +| IX-009 | US-008 | web / desk 通用错误反馈 | 请求或流程失败 | 显示阶段、原因和安全下一步 | P0 | 已定 | +| IX-010 | US-005 | web 围栏前人工处理 | 重置 / 取消 | 关闭旧授权后回 `DRAFT` 或取消 | P0 | 已定 | +| IX-011 | US-005、US-008 | desk 围栏 + web 调和 | 自动申请围栏 / 人工调和 | 点击一次或只处理同一提交 | P0 | 已定 | +| IX-012 | US-010 | web 表格选择与操作条 | 勾选任务 | 明确批量范围、数量、最高总额 | P0 | 已定 | -> IX-009(ERP 建单)随 F-003 移出 MVP,编号保留不重用。 +## 三、P0 高风险交互 -## 三、交互详情(P0 高风险) +### IX-005 开始采购授权 -以下三项涉及资金、不可逆动作或权限,必须完整填写。其余交互保留总表条目。 +**目标**:让管理员用一个明确动作把选中任务授权给采购工具创建待付款订单,同时避免重复授权或 +让用户误以为系统会付款。 -### IX-005 试选确认与下单授权 +- 前置:已登录;至少一条 `DRAFT` 被选中;页面持有每条任务当前版本。 +- 触发:点击“开始采购(只创建待付款订单)”。 +- 点击本身即授权,不再弹出“机器选对了吗”或试选确认,也不需要第二个同义确认按钮。 -- 关联用户故事:US-004、US-005 -- 关联需求 / 验收:F-007、F-008;[需求](02-requirements.md)第五节「决策与资金」 -- 页面 / 组件:web `/tasks/{id}` 路由化详情抽屉 / 完整页的 `SpecTrialCard` + `AuthorizePanel` -- 目标角色:采购管理员 -- 前置条件:已登录;任务状态为 `WAITING_CONFIRMATION`;已收到试选结果与规格面板截图 -- 触发方式:查看后点击「确认下单(不付款)」或「退回,不买」 -- 服务依赖:`POST /tasks/{id}/order-authorizations`、`POST /tasks/{id}/reject` -- 关联原型:待补(`docs/design/`) +操作条必须持续展示: -**这不是候选对照台。** 只有一个商品,人回答的是「机器选对了吗」,不是「哪个更好」。 +1. “已选 N 条”; +2. 所选任务最高总价之和; +3. “采购工具会逐条创建待付款订单,系统不会付款”; +4. 主按钮“开始采购(只创建待付款订单)”。 -**正常路径** - -1. 用户进入详情页,看到需求与机器所选的逐项对照、单价、合计和规格面板截图。 -2. 各项均为 ✓ 时,「确认下单(不付款)」可用。 -3. 用户点击确认,系统签发授权并**锁定本次试选的单价**,任务转 `AUTHORIZED`。 -4. 页面明确提示:只会创建待付款订单,付款需人在拼多多完成。 - -**状态与异常清单** - -| 场景 | 本交互约定 | +| 状态 | 行为 | | --- | --- | -| 默认 / 可操作 | 各项 ✓ 时确认按钮可用;截图必须已加载出来才允许确认 | -| 规格不一致(✗) | **确认按钮禁用**并说明哪一项不符;只能「退回,不买」或转人工 | -| 金额超上限(✗) | **确认按钮禁用**并显示超出多少;不提供「仍然确认」入口 | -| 加载 / 提交中 | 按钮禁用并显示进行中,防重复提交 | -| 成功 | 关闭详情抽屉或返回工作台,该任务显示为已授权并等待桌面端领取第二趟 | -| 退回 | 二次确认后任务终止为 `CANCELED`,说明不会再自动执行 | -| 服务或网络错误 | 保留页面状态,说明原因并允许重试 | -| 权限不足 | 设备凭据调用此接口一律 403 并记录;页面不暴露任务内容 | -| 冲突 / 重复提交 | `expected_task_version` 不匹配返回 409,提示任务已变化并刷新;重复提交幂等,只产生一笔订单 | -| 破坏性操作 | 确认会导致真实下单。提交前必须显示授权金额上限与「系统只下单不付款」 | -| 试选已过期 | 授权过期后任务转待重新试选;完成新试选前不显示确认入口,页面明确说明旧价不再有效 | -| 中断 / 离线 | 未提交的备注不保存;重进页面回到未确认状态 | +| 未选择 | 按钮禁用,说明“请选择待开始任务” | +| 选择中 | 只统计可选 `DRAFT`;非 DRAFT checkbox 禁用且有原因 | +| 提交中 | 按钮禁用并显示“正在授权…”;避免双击,焦点不跳走 | +| 成功 | 表格更新为 `PENDING`,清空已成功选择,toast/实时区提示授权数量和“不付款” | +| 任一版本冲突 | 整批不变,保留筛选,清除失效选择并提示刷新后重选 | +| 网络结果不明 | 使用同一 `start_key` 查询/重放,不生成新 key;不要直接再次授权 | -**可访问性与多端** - -- 确认与退回按钮可键盘到达,焦点可见。 -- ✓ / ✗ 不能只靠颜色区分,必须带文字。 -- 窄屏下截图可放大查看,不被裁切到看不清规格。 +无障碍:操作条出现在表格前,状态变化用礼貌 `aria-live`;按钮具有可见焦点,文案不依赖颜色或图标。 ### IX-006 待付款核对与标记完成 -- 关联用户故事:US-006 | 关联需求:F-009 收口 -- 页面 / 组件:web `/tasks/{id}` 的 `PaymentCheckCard` -- 目标角色:采购管理员(采购执行员在拼多多完成人工付款并反馈结果) -- 前置条件:任务状态为 `WAITING_PAYMENT`,已收到订单截图 -- 服务依赖:`POST /tasks/{id}/mark-paid` +- 页面显示商品、目标与实际规格、数量、授权上限、两次单价、确认页总额和订单截图。 +- 首屏警示:“订单已创建,系统尚未付款。请在拼多多人工核对并付款。” +- “标记为已付款并完成”只写系统状态,不打开支付页面、不触发手机操作。 +- 提交前用简短确认对话框说明这是人工事实记录;成功转 `SUCCEEDED`,重复提交幂等。 +- 信息不一致时提供“转人工处理”,不提供“重新提交订单”。 -**MVP 简化版**:系统只展示,不自动回读。自动核对是 F-016(V2)。 +### IX-008 桌面端轮询与单趟执行 -**正常路径** +采购工具固定两个 tab:“采购执行”(默认)和“配置”。采购执行顶部第一行依次显示采购服务、设备、 +拼多多版本、会话状态与“开始/停止轮询”;不另占一行。 -1. 用户看到订单截图、商品、规格、数量和授权金额。 -2. 用户在拼多多手工核对并付款。 -3. 用户回到页面点「已付款」,任务转 `SUCCEEDED`。 +主体保持左右结构: -**状态与异常清单** +- 左上:当前任务文字占约 2/3,商品图片占右侧约 1/3;无图显示稳定占位。 +- 左下:滚动日志,占用剩余高度;最新日志可自动跟随,用户上滚后不强制跳底。 +- 右侧:采购记录表格,按时间倒序,仅显示标题和状态。 +- 双击/Enter 记录后不弹窗,在左侧显示该记录:左侧原始文字、右侧图片、下方执行结果。 +- 按 Esc 或“返回当前任务”恢复当前任务视图;查看历史不暂停、不取消、不更改服务端状态。 -| 场景 | 本交互约定 | +单趟可见阶段: + +```text +待领取 → 已领取 → 打开商品 → 打开规格面板 → 选择规格 +→ 闸门一 → 数量复核 → 闸门二 → 确认页 / 闸门三 +→ 申请围栏 → 已发出唯一提交 → 待付款 / 待调和 +``` + +| 情况 | 反馈和下一步 | | --- | --- | -| 默认 | 显著提示「对不上就别付」,并说明不一致时回来标记异常而不是在拼多多改单 | -| 成功 | 任务转已完成,记录标记人与时间 | -| 金额或规格对不上 | 提供「标记异常」转 `NEEDS_MANUAL`,不允许直接标已付款 | -| 截图缺失 | 说明证据缺失并转人工,**不允许在无证据情况下标记完成** | -| 重复标记 | 幂等,不产生第二条完成记录 | -| 破坏性操作 | 本交互不产生任何写平台动作,只改本地状态 | +| 服务/设备/App 未就绪 | 开始轮询禁用;状态区给出具体修复入口 | +| 正在领取或执行 | 显示阶段与任务;停止轮询只阻止下一次领取,不中断当前安全步骤 | +| 规格、价格、数量或确认页失败 | 记录固定 reason code,停止点击并进入待人工 | +| 正在申请围栏 | 禁用停止当前任务、退出和任何提交动作;界面只显示进度 | +| 围栏响应不明 | 不点击,进入待调和 | +| 已围栏 / 点击结果不明 | 红色安全横幅“可能已创建订单,禁止再次提交”;只能调和 | +| 外部支付或安全校验 | 立即停止,不与页面继续交互;保存允许的证据并提示人工接管 | +| 连续轮询失败 | 达阈值自动停止会话;当前已围栏提交不受影响 | -### IX-008 桌面端定时轮询与人工接管 - -- 关联用户故事:US-003、US-008 | 关联需求:F-005、F-011 -- 页面 / 组件:desk 采购执行页 `PollControls` + `DeviceStatusBar` + `CurrentTaskPanel` + - `ExecutionLog` + `ExecutionRecordTable` + `ExecutionRecordDetailPanel` -- 目标角色:采购执行员 -- 前置条件:web 端连接正常;设备已通过连接检查 - -**正常路径** - -1. 用户确认 web、设备和已取证 App 版本三项状态都就绪,在顶部状态区点击「开始轮询」。 -2. 每到间隔时间领取一条任务,界面显示**这一趟是试选还是下单**、商品、规格、当前步骤。 -3. 第一趟试选完成后**退出商品页释放手机**,继续下一轮轮询。 -4. 执行结果按时间倒序进入右侧两列表格;单击只选中,双击、Enter 或可见查看命令在左侧打开 - 记录详情。宽屏可继续选择其他记录;可见返回按钮或 `Esc` 回到当前任务并恢复当前行焦点。 - -**状态与异常清单** - -| 场景 | 本交互约定 | -| --- | --- | -| 默认 | 任一就绪条件不满足时「开始轮询」禁用,并说明是 web、设备还是 App 版本问题 | -| 轮询中 | 显示下次轮询倒计时与连续失败计数 | -| 执行中 | 显示当前趟次与步骤;**真机步骤期间禁用硬取消和关闭窗口** | -| 当前任务图片 | 无可信图片时显示空态;有图时标来源与采集时间,不用其他来源图片凑合 | -| 执行记录详情 | 左栏在当前任务 / 记录详情间切换,不打开模态框;详情内左侧原始文字、右侧图片证据、下方结构化采购结果;顶部实时状态不中断;不提供下单、重试提交或付款动作 | -| 详情读取异常 | 加载、失败、无图和记录不存在分别说明;仅可重试读取同一稳定记录 ID,过期异步结果不得覆盖当前视图 | -| 无可领任务 | 显示「暂无待领任务」并继续下一轮,**不当作错误、不计入失败计数** | -| 待人工 | 整页显著变色,说明缺什么、下一步做什么;该任务不再被本端领取 | -| 连续失败 | 达到阈值自动停止轮询并显示原因,**不无限重试** | -| 关闭窗口 | 立即停止轮询,不留后台进程 | -| 设备断连 | 当前任务标技术失败、保留现场;**不重试任何可能创建订单的点击** | -| 安全校验 | 立即停止轮询、截图、转人工,**不提供绕过入口** | -| 外部支付交接 | 立即停止并提示「订单可能已创建、支付未完成」;**不提供「继续」按钮** | -| 点击提交后超时 | 转 `RECONCILIATION_REQUIRED` 并预留金额额度,**禁止自动重试**;授权保持永久围栏 | -| 闸门二不通过 | 说明「价格已变:授权 ¥X,现价 ¥Y」并转人工,不自动下单也不自动放弃 | -| 运行中变更 | 冻结设备切换和参数保存 | -| 服务端不可达 | 保留本地结果与证据,提示待补传,不丢弃已完成工作 | +键盘:Tab 顺序与视觉一致;表格上下箭头移动当前行,Enter 查看,Shift+F10 打开上下文菜单;Esc +优先关闭菜单,其次返回当前任务视图。Esc、关闭窗口和停止轮询都不能释放围栏或触发重试。 ### IX-011 提交围栏与结果调和 -- 关联用户故事:US-005、US-008 | 关联需求:F-017 -- 页面 / 组件:desk 采购执行页;web 任务详情的提交围栏摘要 -- 前置条件:已完成 dry-run;真实第二趟重新通过三道闸门;授权尚未消费且未建立围栏 -- 服务依赖:`order-dry-runs/start`、`order-dry-runs/{rid}/ready`、 - `order-submissions/start`、`order-submissions/{sid}/reconcile`、`manual-review` +围栏由后台流程自动申请,不向执行员提供“提交订单”按钮。 -**正常路径** +1. 当前任务在同一趟内完成闸门二和闸门三,并确认提交控件精确唯一。 +2. 客户端显示“正在申请提交围栏”,提交当前验证摘要。 +3. 只有服务端首次明确返回 `click_permitted=true`,客户端才先持久化唯一点击意图,再精确点击一次。 +4. 明确创建转待付款;外部支付、安全校验、超时、断连或未知页面转待调和。 +5. 围栏后的 web 详情只显示同一 `submission_id` 的证据、调和记录与人工结论。 -1. dry-run 以醒目的「只读演练」标识运行,到确认页读取并验证后退出,不出现提交动作。 -2. 真实第二趟在三道闸门通过后显示「正在申请提交围栏」,此时不允许点击或取消。 -3. 只有服务端明确返回 `click_permitted` 后执行一次点击,界面立即进入「正在核对订单结果」。 -4. 明确创建后转待付款;结果不明确则两端都显示同一 `submission_id` 与「可能已创建」, - 只提供人工核查,不提供重试或放弃。 +| 状态 | 允许动作 | 禁止动作 | +| --- | --- | --- | +| 围栏前 | 停止并转人工;管理员可安全重置为 DRAFT | 绕过闸门、客户端自签授权 | +| 围栏申请结果不明 | 不点击,记录待调和 | 换 key 申请、点击猜测 | +| 围栏已建立 | 上报一次观察结果、人工调和 | 释放授权、重新开始、再次点击 | +| 明确待付款 | 人工核对/付款/标记完成 | 系统支付 | -**状态与异常清单** +围栏重放响应不授予点击许可。调和页面不得出现普通“重试”按钮。 -| 场景 | 本交互约定 | -| --- | --- | -| dry-run | 蓝色信息态并固定显示「不会提交订单」;完成后有证据摘要 | -| 围栏申请中 | 主操作禁用、显示进度;关闭窗口受控,避免用户误以为可重来 | -| 围栏申请失败 / 响应不明 | **不点击**;展示幂等键与核查入口 | -| 围栏成功 | 显示唯一提交编号;只允许内部流程点击一次,不向用户暴露第二个提交按钮 | -| 点击后明确创建 | 转待付款并显示订单证据 | -| 点击后超时 / 外部支付 / 安全校验 | 转 `RECONCILIATION_REQUIRED`;显著提示可能已创建并预留额度 | -| 重复打开或恢复 | 恢复同一提交编号和调和状态,不重新领取、不重新点击 | -| 人工核查 | 可记录「已创建 / 未创建 / 仍不明确」及证据;系统本身不发起新的下单 | +### IX-012 批量选择 + +- 第一列为 checkbox;表头全选只覆盖当前筛选结果中可授权的 `DRAFT`。 +- 行单击只改变当前/焦点行,不等于勾选;双击行打开详情,不切换 checkbox。 +- 状态变化、翻页或筛选后失效的选择必须清除并告知,不能静默作用于不可见旧数据。 +- 不在每行增加“开始采购”操作列;批量操作统一位于表格上方上下文操作条。 +- 移动端表格允许水平滚动,不把关键金额和授权动作压成不可读卡片。 ## 四、通用要求 -适用于所有 P0 交互: - -- **禁用按钮必须说明原因**,不留用户猜。 -- **失败必须给下一步**,不只报错。 -- **空状态是邀请,不是错误**。 -- 破坏性与不可逆动作必须二次确认,并说明影响范围。 -- 键盘可达、焦点可见、`prefers-reduced-motion` 生效。 -- 状态不能只靠颜色表达。 -- 金额一律显示两位小数并标注币种。 -- 双击只能作为效率快捷方式;任务编号链接和聚焦行后按 Enter 必须提供等价详情入口。 -- 表格行内的复选框、商品外链和任务编号链接不得误触发行双击;关闭详情抽屉后焦点回到原行。 -- 表格批量栏只在存在选择时出现;全选只覆盖当前筛选结果中允许当前动作的行。 +- 中文文案,状态名面向用户;技术 code 只出现在可复制详情中。 +- 所有输入有可见 label,错误与字段关联;必填不只靠星号。 +- 可交互元素有键盘可见焦点,目标尺寸至少 40–44 px;不依赖 hover 才能发现关键操作。 +- 加载、空态、失败、离线、禁用、成功、超时和结果不明均有明确表现。 +- 尊重 `prefers-reduced-motion`;状态变化不使用闪烁或大幅位移动画。 +- 高影响动作要说明范围与后果。开始采购已由持续可见的操作条和具体按钮文案完成确认,不再叠加 + 一个同义模态框;取消、人工付款事实等不同后果仍按各自流程确认。 +- 关闭抽屉、返回列表或查看历史记录必须恢复触发点焦点和筛选/滚动现场。 +- 内部截图有权限和不缓存标识;不把地址/手机号做成搜索字段,不展示完整 XML。 ## 五、交付前检查 -- [ ] 每项 P0 交互回链至少一个 US 编号。 -- [ ] 涉及资金或不可逆动作的交互已填写完整状态表。 -- [ ] 所有禁用态都有说明文案。 -- [ ] 所有空状态都有可执行的下一步。 -- [ ] 未在本文自行定义接口路径、字段或状态码。 +- Web:375 / 768 / 1024 / 1440 px;键盘、焦点、表格横向滚动、抽屉返回均检查。 +- Desk:compact / medium / wide;浅色、深色、高对比近似;键盘和窗口关闭路径检查。 +- 单趟状态与 [architecture](04-architecture.md)、[api](api.md) 完全一致。 +- 页面没有 `WAITING_CONFIRMATION`、第二趟、锁定试选价或管理员中途确认。 +- “开始采购”处始终可见“只创建待付款订单 / 系统不付款”。 +- 围栏后没有重试、释放授权或重新开始入口。 diff --git a/docs/README.md b/docs/README.md index a6c2e45..237e4ae 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,8 +4,8 @@ cmbuyer 是一个自动化采购系统:**采购服务**(网页端,`admin/`)负责建单与人工决策, **采购工具**(Windows 桌面端,`client/`)驱动 Android 手机在拼多多完成选规格和下单, -**付款始终由人完成**。第一版先跑通「手工建单 → 定时领取 → 第一趟试选 → 人工确认 → -第二趟下单 → 待付款」闭环。 +**付款始终由人完成**。第一版先跑通“手工建单 → 管理员开始采购授权 → 定时领取 → 单趟规格/ +价格/数量校验 → 提交围栏与一次点击 → 待付款”闭环。 ## 文档导航 @@ -17,7 +17,7 @@ cmbuyer 是一个自动化采购系统:**采购服务**(网页端,`admin/` - [需求](02-requirements.md):功能清单、验收标准、风险点。 - [用户故事清单](07-user-stories.md):US 编号、用户目标与验收场景。 - [技术栈](03-tech-stack.md):两端选型、运行命令、**验证矩阵**。 -- [架构设计](04-architecture.md):双端职责、**两趟执行**、**三道价格闸门**、安全边界、数据模型。 +- [架构设计](04-architecture.md):双端职责、**单趟执行**、**三道价格闸门**、安全边界、数据模型。 - [编码规则](05-coding-rules.md):硬约束,第 1 节是本项目红线。 - [任务路线图](06-tasks.md):阶段划分、里程碑、建议拆分清单。 - [任务文件](tasks/README.md):一任务一文件约定与真机验收要求。 @@ -45,13 +45,13 @@ cmbuyer 是一个自动化采购系统:**采购服务**(网页端,`admin/` 新接手时如果只看四个地方,看这四个: -1. [`04-architecture.md`](04-architecture.md) **第三节两趟执行与三道价格闸门**——为什么 - 分两趟、价格为什么只在两个地方读。 +1. [`04-architecture.md`](04-architecture.md) **第三节单趟执行与三道价格闸门**——开始采购 + 如何授权、两次面板价与确认页金额如何实时保护上限。 2. [`04-architecture.md`](04-architecture.md) **第四节安全边界与提交订单四条件**——每条 都要有测试。 3. [`05-coding-rules.md`](05-coding-rules.md) **第 1 节红线**——违反即拒绝。 4. [`00-ai-start-here.md`](00-ai-start-here.md) **四条特有纪律**——先取证、只收紧、 - 只在两处读价、第一趟不下单。 + 只在两处读价、隔离验证能力不越界。 ## 维护原则 diff --git a/docs/api.md b/docs/api.md index 0d9ac0f..3525caa 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,341 +1,315 @@ # API 合约 -> 本文定义采购服务(`admin/`)对外的 HTTP 接口,以及采购工具(`client/`)本地模块的合约。 -> **这是双端之间的唯一权威。** 实现前可细化,但不得在代码里另起一套不兼容接口。 -> -> 本合约的设备心跳、任务领取、事件、证据、授权命令与 ack 结构参考了前序项目 -> `cmroubao`;同时在本项目重新审计并补回 dry-run、提交前服务端围栏和点击后调和。 -> 前序接口是设计依据,不是可直接照搬的运行事实。 +> 本文是采购服务与采购工具之间的唯一线协议权威。页面判据和本地类名不是线协议。 +> 接口形状参考前序项目的经验,但所有状态与安全语义在 cmbuyer 重新定义、测试和取证。 ## 通用约定 -- 传输:JSON over HTTP。MVP 局域网内运行,生产部署应加 HTTPS。 -- 编码:UTF-8。 -- 时间:RFC 3339,带时区,UTC 存储。 -- 金额:**十进制字符串**(如 `"45.60"`),不用浮点数。 -- 幂等:所有创建类接口接受幂等键,重复提交返回同一结果而不是第二笔。 +- 生产前缀:`/api/v1`;管理页面路由见 [routes.md](routes.md)。 +- JSON 使用 UTF-8;时间为 UTC RFC 3339;ID 为 UUID 字符串。 +- 金额均为规范十进制字符串,如 `"12.88"`;禁止 JSON number 和浮点计算。 +- 所有写接口接受 `request_id` / 业务幂等键;同键同载荷重放同一结果,同键异载荷返回 `409`。 +- 任务写入携带 `expected_task_version`;版本冲突返回 `409 version_conflict`。 +- 服务端错误不得回显设备 token、完整节点树、地址、手机号或支付信息。 -### 鉴权 +### 身份 -| 客户端 | 方式 | 说明 | +| 身份 | 凭据 | 能力 | | --- | --- | --- | -| 管理 Web | Session Cookie + CSRF Token | 表单提交必须带 CSRF | -| 采购工具 | `Authorization: Bearer ` | 凭据绑定设备标识,可单独撤销 | -| ERP 对接 | `Authorization: Bearer ` | 只能调用货运同步接口 | +| 管理员 | `HttpOnly; Secure; SameSite=Lax` 会话 cookie + CSRF | 建单、开始采购、查看内部证据、人工调和 | +| 设备 | `Authorization: Bearer ` + 设备 id | 心跳、领取、事件、截图、围栏与结果 | +| ERP(V2) | 独立凭据 | 只读来源同步,不访问采购结果 | -三种身份互不通用。设备凭据**不能**创建任务或签发授权;管理会话**不能**调用设备接口。 +设备凭据不能建单或开始采购;管理会话不能调用设备接口。未认证统一返回 `401`,无权返回 `403`。 ### 错误响应 ```json { "error": { - "code": "invalid_argument", - "message": "数量必须是正整数", - "field": "quantity" + "code": "version_conflict", + "message": "任务已变化,请刷新后重选", + "retryable": false, + "request_id": "018f..." } } ``` -错误码枚举:`invalid_argument`、`unauthenticated`、`permission_denied`、`not_found`、 -`conflict`、`failed_precondition`、`internal`。 - -- 未登录访问受保护资源:`401`,管理页面重定向到 `/login`。 -- 已登录但无权限:`403`。**不存在**与**无权限**必须使用不同内部原因,但响应体不得泄露 - 任务内容。 +`retryable=true` 只表示接口调用可以按同一幂等键重放,不表示可以重试任何真机点击。 ## 一、管理端接口 -管理页面为服务端渲染,表单直接 POST 到下列路径,成功后 303 重定向。 - -| 方法 | 路径 | 职责 | +| 方法 | 路径 | 作用 | | --- | --- | --- | -| `POST` | `/login` | 建立管理会话 | -| `POST` | `/logout` | 销毁会话 | -| `GET` | `/tasks` | 任务列表,支持 `q`、`status`、`days`、`cursor` | -| `POST` | `/tasks` | 手工建单,初始状态为 `DRAFT`(F-001) | -| `POST` | `/tasks/start-trials` | 批量把 `DRAFT` 原子转为 `PENDING`,开始第一趟试选(F-018) | -| `GET` | `/tasks/{id}` | 任务详情 | -| `POST` | `/tasks/{id}/cancel` | 取消任务 | -| `POST` | `/tasks/{id}/order-authorizations` | 确认试选结果并签发授权(F-008) | -| `POST` | `/tasks/{id}/order-authorizations/{aid}/abandon` | 围栏前放弃授权,任务转待重新试选(F-010) | -| `POST` | `/tasks/{id}/reject` | 退回不买,任务终止 | -| `POST` | `/tasks/{id}/mark-paid` | 人工核对付款后标记完成(MVP 简化收口) | +| `GET/POST` | `/login` | 登录页 / 建立管理员会话 | +| `POST` | `/logout` | 退出并使会话失效 | +| `GET` | `/tasks` | SSR 任务表格;关键词、状态、时间筛选 | +| `POST` | `/tasks` | 手工创建 `DRAFT` | +| `POST` | `/tasks/start-purchases` | 批量开始采购:创建一次性授权并原子转 `PENDING` | +| `GET` | `/tasks/{id}` | 任务完整页;同一 URL 也可由列表详情抽屉加载 | +| `POST` | `/tasks/{id}/reset-to-draft` | 围栏前人工处理后关闭旧授权,回到 `DRAFT` | +| `POST` | `/tasks/{id}/cancel` | 围栏前取消任务 | +| `POST` | `/order-submissions/{sid}/reconcile` | 围栏后人工调和同一提交 | +| `POST` | `/tasks/{id}/mark-paid` | 人工确认已付款并完成核对 | +| `GET` | `/evidence/{asset_id}` | 登录后读取内部截图;`Cache-Control: no-store` | -> Excel 导入(`/tasks/import`)与 ERP 货运(`/freight*`)已移出 MVP,见 -> [需求](02-requirements.md)第三节后续迭代表。 +### `POST /tasks` -### `POST /tasks/start-trials` - -管理页面以带 CSRF 的表单提交结构化任务版本列表: +核心字段: ```json { - "start_key": "<幂等键>", + "create_key": "018f...", + "title": "纯棉短袖", + "product_url": "https://mobile.yangkeduo.com/goods.html?goods_id=937122477375", + "sku_color": "黑色CHA(纯棉)", + "sku_size": "M(建议100-115)", + "quantity": 2, + "max_total_price": "30.00" +} +``` + +- 服务端解析并保存 canonical URL 与 `goods_id`;URL 非拼多多商品页、`goods_id` 缺失或含歧义则拒绝。 +- `max_total_price` 是本任务允许创建待付款订单的总额上限,不是参考单价。 +- 成功只产生 `DRAFT`;不得创建授权、开放设备领取或触发真机。 + +### `POST /tasks/start-purchases` + +```json +{ + "start_key": "018f...", "tasks": [ - { "id": "018f...", "expected_task_version": 1 }, - { "id": "0190...", "expected_task_version": 3 } + {"task_id": "018f-task-1", "expected_task_version": 1}, + {"task_id": "018f-task-2", "expected_task_version": 1} ] } ``` -- 只接受当前状态为 `DRAFT` 的任务;该动作含义是允许采购工具领取第一趟试选,**不签发下单 - 授权、不建立提交围栏、不创建订单、不付款**。 -- 服务端在一个事务内校验全部任务存在、属于当前管理范围、状态仍为 `DRAFT` 且版本匹配, - 然后统一转 `PENDING` 并递增版本。任一项失败返回 `409 conflict`,整批不产生部分成功。 -- `tasks` 为空或包含重复 id 返回 `400 invalid_argument`;重复 `start_key` 返回第一次的结果, - 不重复推进版本。 -- SSR 成功后 `303` 返回原任务列表查询地址;冲突时保留筛选条件,刷新表格并要求重新选择。 +管理员按钮必须显示为“开始采购(只创建待付款订单)”。**点击本身就是授权**:允许采购工具按任务 +锁定字段创建一笔待付款订单;不再等待试选后人工确认,也不授权付款。 -### `POST /tasks/{id}/order-authorizations` +服务端在一个事务中: + +1. 校验列表非空、无重复任务,所有任务均为 `DRAFT` 且版本一致; +2. 校验每条任务的 `goods_id`、规格、正整数数量和最高总价完整; +3. 为每条任务创建一次性 `order_authorization`,锁定任务版本、上述字段、管理员、时间和有效期; +4. 把所有任务转为 `PENDING` 并递增版本。 + +任一条失败则整批不变。相同 `start_key` + 相同任务集合重放同一批结果;集合或版本不同返回 409。 ```json { - "authorization_key": "<幂等键>", - "expected_task_version": 3, - "spec_trial_id": "018f...", - "note": "" + "start_key": "018f...", + "authorized_count": 2, + "tasks": [ + {"task_id": "018f-task-1", "task_version": 2, "authorization_id": "018f-auth-1"}, + {"task_id": "018f-task-2", "task_version": 2, "authorization_id": "018f-auth-2"} + ], + "payment_automated": false } ``` -- **授权内容不由客户端提交。** `goods_id`、规格、数量、`authorized_unit_price` 全部由 - 服务端从 `spec_trial_id` 指向的试选记录取值——人确认的是那一次试选,不是一组自由填写 - 的参数。 -- `expected_task_version` 不匹配返回 `409 conflict`。 -- `total_price_cap` 由服务端按任务的价格上限计算,客户端无法提高。 -- 响应中返回 `expires_at`;围栏建立前超时后授权自动 `EXPIRED`,任务转 - `PENDING_RETRIAL`,必须重新跑第一趟。 -- 已存在 `order_submission` 时,放弃或超时处理返回 `409 conflict`;该授权只能调和结果或 - 转人工核查,不能重新开放为可执行。 -- MVP 没有「选择理由 / 拒绝理由」——那是多候选择一时的留档需求。这里只有可选 `note`。 +### 围栏前重置与围栏后调和 -## 二、设备侧接口(采购工具调用) +- `reset-to-draft` 必须同时校验任务版本、授权 id 与“尚无 `order_submission`”。关闭旧授权后回到 + `DRAFT`;重新开始必须产生新版本与新授权。 +- 一旦存在 `order_submission`,重置、取消、授权过期和重新开始都返回 `409 submission_fenced`。 +- `reconcile` 只能处理指定 `sid`:人工记录“已创建待付款订单”或“确认未创建/无法完成”。它不能 + 触发设备点击、释放围栏或签发新授权。 -全部要求有效设备 Bearer;凭据中的设备标识是权威身份,请求体里的设备字段仅作核对。 +## 二、设备侧接口 -| 方法 | 路径 | 职责 | +| 方法 | 路径 | 作用 | | --- | --- | --- | -| `POST` | `/api/v1/devices/heartbeat` | 上报版本与就绪位,核对服务端活跃任务 | -| `POST` | `/api/v1/tasks/claim-next` | 原子领取或重放;**同时覆盖待试选与已授权两类** | -| `POST` | `/api/v1/tasks/{id}/start` | `CLAIMED → RUNNING`,创建 execution | -| `POST` | `/api/v1/tasks/{id}/heartbeat` | 更新当前步骤与运行租约 | -| `POST` | `/api/v1/tasks/{id}/release` | 未开始时退回 `PENDING` | -| `POST` | `/api/v1/tasks/{id}/events` | 幂等补报执行事件 | -| `POST` | `/api/v1/tasks/{id}/evidence` | 上传证据资产,SHA-256 寻址 | -| `POST` | `/api/v1/tasks/{id}/spec-trial` | **第一趟**:回传试选结果,任务转 `WAITING_CONFIRMATION` | -| `POST` | `/api/v1/tasks/{id}/commands/next` | **第二趟**:拉取或重放已签发的下单授权 | -| `POST` | `/api/v1/tasks/{id}/commands/{cid}/ack` | 落盘后幂等确认命令 | -| `POST` | `/api/v1/tasks/{id}/order-dry-runs/start` | 开始只读演练;绝不消费授权、绝不允许提交 | -| `POST` | `/api/v1/order-dry-runs/{rid}/ready` | 回传确认页只读结果与证据,结束演练 | -| `POST` | `/api/v1/tasks/{id}/order-submissions/start` | **真实点击前**原子建立唯一提交围栏 | -| `POST` | `/api/v1/order-submissions/{sid}/reconcile` | 点击后上报明确或不明确结果,只调和不重试 | -| `POST` | `/api/v1/order-submissions/{sid}/manual-review` | 将围栏后的不确定结果交给人工核查 | -| `POST` | `/api/v1/tasks/{id}/needs-manual` | 转人工,带原因码与证据 | -| `POST` | `/api/v1/tasks/{id}/fail` | 提交结构化失败与证据 | +| `POST` | `/api/v1/devices/heartbeat` | 上报设备、ADB、App 版本和能力状态 | +| `POST` | `/api/v1/tasks/claim-next` | 原子领取一个 `PENDING` 授权任务或重放本设备未结束领取 | +| `POST` | `/api/v1/tasks/{id}/lease/renew` | 续租;只允许当前 claim | +| `POST` | `/api/v1/tasks/{id}/events` | 批量追加结构化步骤事件 | +| `POST` | `/api/v1/tasks/{id}/evidence` | 显式上传一个内部原始截图 | +| `POST` | `/api/v1/purchase-attempts/{aid}/fail` | 围栏前停止并回传失败摘要 | +| `POST` | `/api/v1/purchase-attempts/{aid}/submission-fence` | 提交当前三闸门摘要并原子申请唯一围栏 | +| `POST` | `/api/v1/order-submissions/{sid}/result` | 点击后一次性上报观察结果;只调和不重试 | -> `/candidates`(多候选回传)、`/reference-image`(图搜参考图)、`/order-record` -> (订单自动核对)随 B 路径与 F-016 一并推迟到 V2。 +### `POST /api/v1/devices/heartbeat` + +```json +{ + "device_id": "desk-01", + "client_version": "0.1.0", + "adb_serial": "192.168.0.173:5555", + "android_release": "16", + "pdd_version": "8.17.0", + "state": "READY" +} +``` + +服务端可返回 `app_version_allowed=false`;采购工具必须停止领取,不能只显示警告后继续。 ### `POST /api/v1/tasks/claim-next` -```json -{ "device_id": "desk-01", "claim_key": "<幂等键>" } -``` - -成功: +请求携带 `device_id`、`session_id`、`claim_request_id`。领取与授权绑定且具租约: ```json { "task": { - "id": "018f...", - "leg": "TRIAL", - "goods_id": "7531364299", - "product_url": "https://mobile.yangkeduo.com/goods.html?goods_id=7531364299", - "sku_color": "白色", - "sku_size": "XL", + "id": "018f-task", + "version": 3, + "title": "纯棉短袖", + "product_url": "https://mobile.yangkeduo.com/goods.html?goods_id=937122477375", + "goods_id": "937122477375", + "sku_color": "黑色CHA(纯棉)", + "sku_size": "M(建议100-115)", "quantity": 2, - "max_total_price": "80.00" + "max_total_price": "30.00" }, - "claim_token": "...", - "claim_generation": 1, - "lease_expires_at": "2026-08-03T10:30:00Z" + "authorization": { + "id": "018f-auth", + "task_version": 2, + "expires_at": "2026-08-04T10:00:00Z" + }, + "attempt": { + "id": "018f-attempt", + "claim_token": "opaque-single-claim-token", + "claim_generation": 1, + "lease_expires_at": "2026-08-04T09:05:00Z" + } } ``` -- **`leg` 决定这一趟做什么**:`"TRIAL"` = 第一趟试选,`"ORDER"` = 第二趟下单。 - `leg` 为 `"ORDER"` 时只返回 `authorization_id`;完整、不可变的授权命令必须通过 - `/commands/next` 拉取并落盘,再调用 `/commands/{cid}/ack`。领取接口不重复定义授权载荷。 -- 无可领任务返回 `200` 且 `task` 为 `null`,**不是 404**。 -- 后续所有该任务的调用必须携带 `X-Claim-Token` 与匹配的 `claim_generation`。 +- 只返回有 `ACTIVE` 授权的 `PENDING`;服务端在一个事务中转为 `CLAIMED` 并创建 attempt。 +- 同一 `claim_request_id` 同载荷重放同一结果;并发设备只有一个成功。 +- 一个设备有未结束领取时优先重放该领取,不能悄悄领第二条。 +- 响应不得包含自由动作脚本、CSS/XPath、通用坐标或支付能力。 -### `POST /api/v1/tasks/{id}/evidence`(内部原始截图) +### 事件与证据 -> 本接口属于生产第一趟证据链,由 T-204 实现。T-103 只做本机真机可行性验证,不调用本接口, -> 也不为上传目的继续扩展截图遮罩器。正式流程直接上传原始截图;T-103 与 T-204 的区分只是任务拆分。 +事件只包含固定 `step` / `outcome` / `reason_code` 和非敏感摘要。禁止把完整 XML、地址、手机号、 +页面全文或 token 塞进日志字段。 -这是内部系统。规格面板和订单确认页固定展示的地址与手机号允许保留在原始截图中。本接口只接受截图 -文件及如下审计元数据;完整 XML、manifest、本机路径和目录中的其他文件一律不接收: +截图接口使用 `multipart/form-data`,只接受单个显式文件及以下元数据: ```json { - "kind": "SKU_PANEL_SCREENSHOT", + "attempt_id": "018f-attempt", + "kind": "SKU_PANEL_GATE_1", "privacy_tier": "INTERNAL_RAW", - "artifact_sha256": "<原始截图 SHA-256>", - "pdd_version": "8.17.0" + "sha256": "64-lowercase-hex", + "captured_at": "2026-08-04T09:01:00Z" } ``` -- `privacy_tier` 必须精确为 `INTERNAL_RAW`,明确告知服务端该资产可能包含页面个人信息。 -- 上传截图的 SHA-256 必须等于 `artifact_sha256`。服务端不接收截图以外的原始文件、完整 XML、 - 本机路径、源 manifest 或地址/手机号的结构化字段。支付凭据和外部支付页截图始终拒绝。 -- 原始目录不得被 `HttpResultSink` 或证据上传器枚举;调用方必须显式传入单个截图文件。 -- 完整 XML 永不上传。最小 XML 只用于采购工具离线 fixture;fixture 不得包含地址、手机号或支付凭据。 -- 资产不得放在公开静态目录;读取要求管理会话,响应使用 `Cache-Control: no-store`。 +- 允许规格面板和确认页截图保留页面已显示的地址/手机号;不要求遮罩或裁剪。 +- 不接受 XML、目录、manifest、本机绝对路径、外部支付页截图或支付凭据。 +- MIME、尺寸、字节数和 SHA-256 必须校验;资产只经管理员鉴权端点读取。 -### `POST /api/v1/tasks/{id}/spec-trial`(第一趟回传) +### `POST /api/v1/purchase-attempts/{aid}/submission-fence` + +客户端只有在当前页面四条件中的后三项已经满足后才能调用: ```json { - "attempt": 1, - "product_title": "2026夏季新款纯棉圆领短袖T恤男女同款宽松半袖", - "selected_color": "白色", - "selected_size": "XL", - "unit_price": "32.50", - "total_price": "65.00", - "evidence_sha256": "…" -} -``` - -- `selected_color` / `selected_size` 是**实际勾选到的值**,不是任务要求的值。 - 服务端据此与任务要求比对并在确认页显示 ✓ / ✗。 -- `unit_price` 来自闸门一(规格面板)。**读不到时不要发这个接口**,改发 - `/needs-manual` 并带原因码 `UNIT_PRICE_UNREADABLE`。 -- `total_price` = `unit_price` × 任务数量,服务端会重算校验。 -- 原始截图须先经 `/evidence` 上传,且对应资产必须为 `privacy_tier=INTERNAL_RAW`。 -- 服务端接收后创建 `spec_trials` 记录,任务转 `WAITING_CONFIRMATION`。 - -### dry-run 与真实提交协议 - -`POST /api/v1/tasks/{id}/order-dry-runs/start` 创建或重放一次演练记录。采购工具随后只允许 -进入订单确认页、读取非敏感摘要和验证提交控件唯一,不允许点击。完成后调用 -`POST /api/v1/order-dry-runs/{rid}/ready`: - -```json -{ - "command_id": "…", - "verified_unit_price": "32.50", + "fence_key": "018f-fence-request", + "task_id": "018f-task", + "expected_task_version": 3, + "authorization_id": "018f-auth", + "claim_token": "opaque-single-claim-token", + "selected_color": "黑色CHA(纯棉)", + "selected_size": "M(建议100-115)", + "gate1_unit_price": "12.88", + "gate2_unit_price": "12.88", "quantity_read": 2, - "confirm_page_amount": "65.00", - "has_address": true, - "evidence_sha256": "…" + "confirm_page_amount": "25.76", + "submit_control_match_count": 1 } ``` -- dry-run 只证明当次页面达到 `READY`,不冻结授权,也不能作为稍后真实点击时的页面事实。 -- `has_address` 只报布尔值,**不得回传地址原文或手机号**。 +服务端在一个事务中校验:任务/版本/claim/attempt 一致;授权有效未消费且字段等于任务快照; +规格与授权相等;数量相等;两个单价相等;计算金额及确认页金额均不超过 `total_price_cap`;提交控件 +计数为一;此前不存在该授权或 attempt 的 submission。随后创建唯一 `order_submission`,授权转 +`FENCED`,任务保持不可重领。 -真实第二趟重新通过三道闸门后,采购工具在点击前调用 -`POST /api/v1/tasks/{id}/order-submissions/start`: +首次明确成功响应: ```json { - "submission_key": "<幂等键>", - "command_id": "…", - "dry_run_id": "…", - "expected_task_version": 5, - "verified_unit_price": "32.50", - "quantity_read": 2, - "confirm_page_amount": "65.00" + "submission_id": "018f-submission", + "status": "FENCED", + "click_permitted": true, + "submit_text": "提交订单" } ``` -- 服务端在一个事务中校验命令、任务版本、授权未消费、闸门值与唯一性,创建或重放唯一 - `order_submission` 并把授权置为 `FENCED`。同一授权或命令不得产生第二条提交记录。 -- 只有明确收到 `201/200` 且响应中的 `click_permitted: true`,采购工具才允许点击一次。 - 超时、网络错误、冲突或响应无法解析时**不得点击**,转人工查询该幂等键。 -- `dry_run_id` 只证明曾完成安全演练;服务端仍以本次真实提交请求携带的闸门读数复核。 +- 任一校验失败返回错误,绝不返回 `click_permitted=true`。 +- 同一 `fence_key` 的重放返回同一 `submission_id`,但 `click_permitted=false` 且 + `reconciliation_required=true`;客户端不能凭重放响应点击。 +- 客户端收到首次许可后,必须先把“围栏已取得/即将发出唯一点击”持久化,再执行点击。进程崩溃或 + 本地状态不明时宁可转调和,也不再次点击。 -点击后调用 `POST /api/v1/order-submissions/{sid}/reconcile`: +### `POST /api/v1/order-submissions/{sid}/result` ```json { - "outcome": "SUBMITTED", - "evidence_sha256": "…" + "result_key": "018f-result", + "attempt_id": "018f-attempt", + "observation": "SUBMITTED", + "evidence_asset_id": "018f-asset" } ``` -- `outcome` 枚举:`SUBMITTED`(明确看到订单结果)、`UNCERTAIN`(超时或无法判断)、 - `HANDED_OFF`(外部支付)、`SECURITY_CHECK`。 -- `SUBMITTED` 转 `WAITING_PAYMENT`;其余一律转 `RECONCILIATION_REQUIRED`,授权保持已围栏并 - 预留金额额度。重复调用只重放同一调和结果。 -- 任一结果都**禁止再次点击、释放围栏或重新签发授权**。无法自动调和时调用 - `/manual-review` 记录人工核查请求与证据。 +`observation` 只允许: -### 文本字段校验 +- `SUBMITTED`:明确订单已创建,转 `WAITING_PAYMENT`; +- `EXTERNAL_PAYMENT_HANDOFF`:已跳外部支付,停止并转 `RECONCILIATION_REQUIRED`; +- `SECURITY_CHALLENGE`:出现安全校验,停止并转调和; +- `UNKNOWN`:超时、断连或页面不明,转调和。 -所有自由文本字段(事件消息、失败原因、备注): +提交后没有“retry”观察值。任何结果都不能释放围栏或开放第二次点击。 -- UTF-8,有长度上限(事件消息 1000 字节,备注 500 字节)。 -- 拒绝含 `authorization:`、`api_key`、`bearer ` 的内容,防止凭据误入审计日志。 -- **超长必须由客户端截断后再发,服务端拒绝而不是静默截断。** +### 文本和金额校验 + +- 规格字段:Unicode 规范化后精确相等;不得包含、前缀、编辑距离或 AI 猜测。 +- `goods_id`:仅 ASCII 十进制数字,canonical URL 中唯一。 +- 金额:`0.01` 到系统配置上限,至多两位小数;规范化后再比较和持久化。 +- 数量:正整数,服务端与设备均设置合理上限;不能从字符串静默截断。 ## 三、采购工具本地模块合约 ### `TaskSource` / `ResultSink` -执行器只依赖抽象,不认识来源: - ```python -class TaskSource(ABC): - @abstractmethod - def load_tasks(self) -> list[OrderTask]: ... +class TaskSource(Protocol): + def claim_next(self, session: Session) -> ClaimedPurchase | None: ... + def renew_lease(self, claim: Claim) -> Lease: ... -class ResultSink(ABC): - @abstractmethod - def save_task_result(self, task: OrderTask) -> None: ... +class ResultSink(Protocol): + def append_events(self, claim: Claim, events: list[TaskEvent]) -> None: ... + def upload_screenshot(self, claim: Claim, asset: ScreenshotAsset) -> AssetRef: ... + def fail_attempt(self, claim: Claim, failure: AttemptFailure) -> None: ... + def create_submission_fence(self, claim: Claim, proof: SubmissionProof) -> SubmissionPermit: ... + def report_submission_result(self, permit: SubmissionPermit, result: SubmissionResult) -> None: ... ``` -实现: +执行器不能依赖具体 HTTP 或 Excel 实现。`SubmissionPermit` 只能由 `ResultSink` 的服务端成功响应构造, +业务代码不能手工 new 一个许可。 -| 实现 | 用途 | -| --- | --- | -| `HttpTaskSource` | 从采购服务领取任务(默认) | -| `HttpResultSink` | 回传结果到采购服务(默认) | -| `FixtureTaskSource` | 仅测试 / 演示:读取仓库内假数据,不接触真实订单 | -| `JsonlResultSink` | 仅测试 / 断连暂存:本地追加写入,恢复连接后按幂等键补传 | +### 真机能力分层 -### 真机流程模块 +| 能力 | 输入 | 输出 | 安全边界 | +| --- | --- | --- | --- | +| `open_product()` | canonical URL + 证据版本 | 已确认商品页 | URL、前台包、App 版本全部匹配 | +| `open_sku_panel()` | 版本绑定受控入口 | 已确认规格面板 | 精确唯一;无通用 click | +| `select_sku_options()` | 维度 → 精确值 | 选中态摘要 | 维度内唯一匹配并读回 | +| `read_sku_unit_price()` | 已确认规格面板 | 十进制单价 | 排除原价、按钮价和歧义候选 | +| `set_quantity_and_readback()` | 授权数量 | 实际数量 | 精确读回,否则停 | +| `go_to_order_confirm()` | 已通过闸门二 | 确认页摘要 | 后续真机任务取证后才实现 | +| `submit_order_once()` | 不可伪造的首次 `SubmissionPermit` | 观察结果 | 许可、闸门、唯一控件全校验;点前持久化;绝不重试 | -`client/src/android/pdd_flow.py` 的公开入口按 capability 分离,每个都不得越界: +T-103 只实现隔离的 `SkuSelectionFlow`:前四项加安全退出。它的模块和静态依赖不得引用数量、确认页、 +围栏、提交或支付能力。后续任务按取证顺序组合成生产 `SinglePassPurchaseFlow`。 -| 函数 | 可用趟次 | 输入 | 输出 | 副作用边界 | -| --- | --- | --- | --- | --- | -| `open_product(url)` | TRIAL / ORDER | 商品 URL | 页面快照路径 | 只打开页面,不点击控件 | -| `open_trial_sku_panel(evidence_key)` | **仅 TRIAL** | 版本与证据绑定键 | 面板快照 | 只点击精确唯一、已取证的受控入口;当前仅 `快要抢光`,无通用 click | -| `select_sku_options(items)` | TRIAL / ORDER | `{维度: 值}` | 选中证据 | 按维度精确匹配,找不到抛错 | -| `capture_evidence(screenshot)` | TRIAL / ORDER | 单个原始截图路径 | 截图 SHA-256 | 只处理显式截图文件;不得枚举目录或读取/上传 XML,T-204 接入内部证据 API | -| `read_sku_unit_price(xml)` | TRIAL / ORDER | 当前规格面板 XML 或安全最小 fixture | 单价或 `None` | **闸门一 / 二**;只返回金额摘要,读不到返回 `None`,不猜、不返回页面其他正文 | -| `leave_product()` | TRIAL / ORDER | - | - | 第一趟结束时退出并释放手机 | -| `set_quantity(n)` | **仅 ORDER** | 数量 | 读回值 | 必须复核等于 n;TRIAL capability 不暴露 | -| `go_to_order_confirm()` | **仅 ORDER** | - | 确认页摘要 | 需显式授权;TRIAL capability 不暴露 | -| `read_order_confirm_info(xml)` | **仅 ORDER** | 脱敏页面 XML | 非敏感摘要 | **闸门三**;不提取地址原文、手机号 | -| `submit_order(auth, submission)` | **仅 ORDER** | 授权 + 已建立的提交围栏 | 提交结果 | **唯一创建真实订单入口**,四条件与围栏全通过后只点一次 | +## 四、实现前仍需定值 -能力隔离规则: - -- `go_to_order_confirm()` 必须校验授权存在;`submit_order()` 还必须校验授权已由服务端围栏 - 且 `submission` 与当前任务、命令、授权完全一致。 -- 第一趟只能拿到 `TrialSkuFlow` 窄接口,接口中不得出现通用 `click`、`set_quantity()`、 - `go_to_order_confirm()`、`submit_order()` 或支付能力;静态依赖测试必须证明试选流程不可达它们。 -- `open_trial_sku_panel()` 的点击是唯一批准的购买语义控件例外,只用于打开已取证规格面板;入口 - 缺失/重复、App 版本不符、面板判据不唯一或出现未知终态控件时停止。其他入口文案不得推断复用。 -- `search_by_image()` 属 B 路径,V2 再实现。 - -## 四、待实现时确认 - -- **规格面板上单价的节点位置与文本形态**(阻塞闸门一,由 T-103 真机取证确定)。 -- 授权 `expires_at` 的默认时长。 -- 定时轮询的默认间隔与连续失败停止阈值。 -- 分页游标的编码方式。 -- 设备凭据的有效期与轮换策略。 -- 证据资产的保留期与清理策略。 +- 授权有效期、领取租约时长、心跳/轮询间隔和连续失败停止阈值; +- 截图大小上限和内部保留期限; +- 可配置单任务数量与最高总价系统上限; +- 首次真实提交真机任务的人工授权和待付款订单处置步骤。 diff --git a/docs/current-state.md b/docs/current-state.md index ca67e55..baf44de 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -12,8 +12,10 @@ ## 当前快照 - 日期:2026-08-04 -- 阶段:**Phase 1 · 真机可行性(T-110 已批准受控规格入口,T-103 重新执行)** -- MVP 形态:手工填链接建单 → 批量开始试选 → 定时轮询 → **第一趟试选** → 人工确认 → **第二趟下单** → 待付款 +- 阶段:**Phase 1 · T-103 规格选择/读价与 Phase 2 安全服务端任务并行** +- MVP 形态:手工填链接建单 → 批量“开始采购”并签发一次性授权 → 定时领取 → 同一趟精确 + 选规格 / 三道价格闸门 / 提交围栏 / 点击一次 → 待付款。管理员点击“开始采购”即授权桌面端 + 创建待付款订单;不再有试选后确认,系统绝不自动付款。 - 技术栈:已定。采购服务(`admin/`)使用 Go 1.23+ / gin / SQLite;采购工具(`client/`) 使用 Python 3.11+ / uiautomator2 / PySide6。 详见 [`03-tech-stack.md`](03-tech-stack.md) @@ -23,27 +25,30 @@ 以及绑定 PKG110 / Android 16 / 拼多多 8.17.0 的规格证据确定性脱敏 CLI;尚无规格选择、价格读取或下单流程 - 测试:采购服务已覆盖健康检查、核心模型、迁移与状态机等离线包级测试;采购工具 80 项离线单元测试 (全部 mock,不连接真机) -- 数据:SQLite 核心表与迁移已落成;无业务实例数据 +- 数据:SQLite v1 核心表与迁移已落成,但仍是旧两趟 schema(含 `spec_trials`、 + `authorized_unit_price` 和旧状态);无业务实例数据。T-111 只冻结目标契约,不改生产代码;T-209 + 必须先迁移 schema / 领域状态机,T-203 才能实现新“开始采购”事务。 - 标准启动路径:Windows PowerShell 运行 `./init.ps1`,Unix shell 运行 `./init.sh`。Windows 入口 优先使用合规的既有 venv;仅在其缺失时才从 Python Launcher 已安装版本中选择最高的 Python 3.11+, 并且不覆盖低版本环境;成功后打印真实启动命令。 - 标准验证路径:`./init.ps1` 已实际跑通 admin 的 mod download / test / vet / build、client 的 editable install / 包导入 / unittest / compileall,以及仓库上下文校验。可单独运行两端命令诊断。 - 当前设备门禁:人工已确认拼多多 8.17.0、goods_id `937122477375` 的衣服商品只能通过“快要抢光” - 打开规格面板;T-110 已获项目所有者批准,只把该证据/版本绑定的精确唯一入口作为第一趟可逆导航, - 数量、确认页、提交订单、付款与通用点击能力仍不可达。T-103 的 v5 派生 XML 已由人确认无实际地址/ + 打开规格面板;T-110 已获项目所有者批准,只把该证据/版本绑定的精确唯一入口作为受控导航。 + T-103 的隔离 capability 仍不得包含数量、确认页、提交订单、付款或通用点击;这些动作由后续任务 + 分别取证后才接入生产单趟流程。T-103 的 v5 派生 XML 已由人确认无实际地址/ 手机号,并保留目标预选规格、顶部当前价“快卖完 ¥12.88”和原价“¥29.88”;底部“提交订单 ¥12.88” 继续属于硬拒绝区。派生截图虽然把顶部价格遮住一半,但项目已停止遮罩器开发,视觉完整性不再阻塞 - 规格选择与读价;T-204 将直接上传内部原始截图供管理员查看。T-010 已允许不依赖真机字段的 T-201 和只创建 - `DRAFT` 的 T-202 并行; - T-203 及后续会启动试选或依赖真机字段的 Phase 2 功能继续等待 T-103。 + 规格选择与读价;T-204 将直接上传内部原始截图供管理员查看。T-010 已允许不依赖真机字段的 T-201 + 和只创建 `DRAFT` 的 T-202 并行。T-209 的 schema / 状态机迁移不依赖页面选择器,可在 T-111 + 完成后推进;随后做 T-203 服务端“开始采购”授权事务。T-205 起实际 attempt / 真机字段继续等待 T-103。 ## 当前目录要点 | 路径 | 状态 | 说明 | | --- | --- | --- | | `docs/` | 已有 | 项目规范化文档,本次已完整生成 | -| `docs/tasks/` | 已有(T-001~T-004、T-005~T-009、T-101~T-110) | T-001~T-004、T-101~T-102、T-110 已完成;T-103 重新执行 | +| `docs/tasks/` | 已有(含 T-001~T-111、T-201~T-202) | T-111 单趟契约已完成;T-103 已恢复;T-202 在独立工作树待主审提交 | | `docs/design/` | 已有(6 个原型) | web 登录 / 建单 / 工作台 / 详情,desk 采购执行 / 配置;均已人工确认 | | `scripts/` | 已有 | 上下文门禁、Vikunja 单向导出与 MCP 启动包装 | | `admin/` | 已初始化 | Go 1.23+ / gin / SQLite,含核心模型、迁移与状态机;无真机采购执行 | @@ -60,12 +65,11 @@ - 已完成:T-002(采购工具 Python 骨架)、T-003(双端统一初始化与验证入口)、 T-004(核心数据模型)、T-101(真机环境盘点与 USB/WiFi 双通道人工验收)、T-102(canonical 链接打开与目标商品/隐私人工验收)。 -- 已完成 T-010(安全并行门禁);T-201(管理员登录与会话)已在独立写路径并行开发。T-202 可在 - T-201 完成后继续,但只能创建和展示 `DRAFT`,不得启动试选或引入未经 T-103 证实的真机字段。 -- 已完成 T-110(第一趟受控规格入口与隐私脱敏边界)。T-103 是当前最高优先级和 MVP 生死线, - 已补充 T-110 依赖并保持 `DOING`;现有安全派生 XML 足以开始最小 fixture、规格选择与读价实现, - 内部原始截图上传与展示交给 T-204;T-103 最终真机验收完成前不开发 - T-203 及后续依赖真机可读字段或会启动试选的 Phase 2 生产页面。 +- 已完成 T-010(安全并行门禁)与 T-201(管理员登录与会话)。T-202 已由 admin agent 实现且 + 测试通过,仍只创建/展示 `DRAFT`;当前在独立工作树等待主 agent 审阅、提交和推送。 +- 已完成 T-110(受控规格入口边界)与 T-111(开始采购授权的单趟契约)。T-103 已恢复为 + `DOING`,以 `SkuSelectionFlow` 继续最小 fixture、精确规格和读价,不实现数量、确认页或提交; + 内部原始截图上传交给 T-204。admin 方向在 T-202 主审合入后,先落 T-209 迁移旧 schema/状态机。 - 已确认原型继续只作信息架构依据;原型假数据不调用真实接口、不驱动真机。真机结论改变 可读字段时必须先回修原型与交互清单。 @@ -152,7 +156,7 @@ T-103 已确认当前衣服商品只能从精确文案“快要抢光”进入 本轮不再运行或修改截图遮罩器。T-103 先用本机证据验证真机能力;T-204 再把原始规格页截图上传采购 服务,允许页面中已显示的地址和手机号,完整 XML 不上传。代码从已确认的派生 XML 提取最小 fixture, -并在真机运行时只返回获准摘要。下一步实现 `TrialSkuFlow` 的受控入口、 +并在真机运行时只返回获准摘要。T-111 完成后,下一步实现 `SkuSelectionFlow` 的受控入口、 按维度精确选择、选中态读回、SKU 单价读取与安全退出;完成离线主审后,由人先把两个维度改成明确 记录的非目标值,再执行真机脚本确认恢复目标规格与 `12.88` 单价。生产原始截图展示/上传由 T-204 处理。 @@ -171,14 +175,14 @@ T-103 已确认当前衣服商品只能从精确文案“快要抢光”进入 ## 已知风险(开工前须知) 1. **M2 是生死线**:真机能按链接打开商品、精确勾选颜色分类和尺码、**读到该 SKU 单价** - (T-103)。Phase 1 不通过之前不要写生产页面;Phase 0 原型只确认流程和信息架构,真机 - 结论改变字段时必须回修。 + (T-103)。M2 前可以写不依赖页面字段的管理员会话、DRAFT 建单和服务端授权事务;不能写 + 依赖未取证选择器的生产执行代码。真机结论改变字段时先回修契约与原型。 2. **拼多多页面结构随版本变化**,已观察到详情页无独立规格入口、价格节点被拆分等情况。 3. **授权卡死**:前序项目出现过 `EXECUTING` 授权永不推进导致任务锁死。本项目在 T-207 实现围栏前超时 / 放弃,在 T-208 实现围栏后调和;围栏后不得释放或重试。 -4. **规格面板单价位置未取证**:闸门一依赖它,T-103 必须一并取证。若读不可靠, - 确认页设计要改。 -5. **MVP 已收窄**:只做手工填链接、批量开始第一趟试选和两趟执行。Excel、ERP、图搜、 +4. **规格面板读价尚未形成生产判据**:已人工观察当前价与原价,但 T-103 仍需把证据固化为 + 唯一、版本绑定且排除按钮价的读取规则;失败就转人工,不删价格闸门。 +5. **MVP 已收窄**:只做手工填链接、批量开始采购和单趟创建待付款订单。Excel、ERP、图搜、 批量顺序编排 / 暂停接管、订单自动核对、AI 辅助全部推到 V2(见 `06-tasks.md` 的 T-501~T-508)。 6. **App 版本必须 fail closed**:运行时拼多多版本与本项目已取证版本不一致就停止领取, diff --git a/docs/design/README.md b/docs/design/README.md index 8fe34b4..87ba31f 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -51,6 +51,8 @@ - **开工门槛(一次性)**:P0 的 UI 模块首次实现前应有原型;没有就先生成原型、人工确认后再拆任务。 - **两阶段确认**:Phase 0 先确认流程、状态、主动作和布局;T-103 真机取证后再核对实际可读 字段与文案。第二阶段若有变化,先修订 IX 与原型,再写生产页面。 +- 管理员“开始采购”授权的单趟流程属于显著改版,T-111 重新生成任务创建、工作台、详情和桌面执行 + 四个原型;旧的试选后确认/第二趟状态不得继续作为实现输入。 - **触发式重新生成**:新需求显著改变某页面的布局或控件集合时,把"重新生成该页原型 → 更新 IX 草稿"作为该任务的第一步。判断标准只有一条:这次变更是否让 agent 需要重新"看图"才能枚举交互。换文案、加字段等小改动只改 IX 条目,不碰原型。 - **实现后即过期**:页面实现后,原型自动视为过期,不回头修补;实现后的视觉事实由任务文件 `## 执行记录` 中的真实截图或可运行验证承担。 - 需要新原型时整页重新生成,不逐次修补旧文件。 diff --git a/docs/design/desk-execution.html b/docs/design/desk-execution.html index 4aa7b54..feca171 100644 --- a/docs/design/desk-execution.html +++ b/docs/design/desk-execution.html @@ -398,12 +398,12 @@ - - - - - - + + + + + + @@ -469,7 +469,7 @@
商品
暂无商品;轮询会在安全就绪后领取一条任务
颜色分类
—
尺码 / 数量
—
-
授权单价
—
+
闸门一单价
—
金额上限
—
提交围栏
未建立
@@ -583,41 +583,41 @@ const task = { id: 'CB-20260803-0042', product: '纯棉圆领短袖 T 恤(原型假数据)', - color: '白色', sizeQty: 'XL / 2 件', authPrice: 'CNY 32.50', cap: 'CNY 80.00' + color: '白色', sizeQty: 'XL / 2 件', gate1Price: '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 点击结果不明确,转人工核查'], + 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:'这只是试选结果,不代表已下单。' + status:'ORDERING', spec:'透明 | L | 2 件', cap:'CNY 60.00', submission:'未建立', + note:'管理员已开始采购,当前单趟已完成规格与闸门一,正在设置并复核数量。', evidence:['商品页截图(内联假证据)','规格面板原始截图(内联假证据)'], + timeline:['12:22:31 领取一次性授权','12:23:04 精确匹配透明 / L','12:23:38 闸门一读取 CNY 24.00','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 明确创建待付款订单'], + 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:[], + note:'设置数量后规格面板价格与闸门一不一致,已在围栏前转人工。', evidence:[], timeline:['09:16:40 打开商品','09:17:22 匹配透明 / 45mm','09:18:05 价格闸门二不通过,停止自动流程'], - result:'未采购:授权价 CNY 12.00 | 当前价 CNY 13.50', resultNote:'没有创建订单,也没有付款。' + 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'}, + ORDERING:{label:'采购执行中',tone:'info'}, WAITING_PAYMENT:{label:'待付款',tone:'success'}, NEEDS_MANUAL:{label:'转人工',tone:'caution'} }; @@ -627,40 +627,38 @@ let detailRequestId = 0; const baseSteps = { idle: [], - trial: ['打开商品(待真机判据)', '精确匹配颜色分类与尺码', '从规格面板读取单价', '截图并退出商品页'], - dry: ['重新打开商品(待真机判据)', '精确选择与数量复核', '读取订单确认页非敏感摘要', '验证提交控件唯一后退出'], - order: ['重新选择同一规格', '闸门二:单价必须等于授权价', '数量复核与闸门三', '申请服务端提交围栏', '围栏许可后内部仅点击一次', '调和同一提交结果'] + single: ['核对一次性授权并打开商品', '精确选择颜色分类与尺码', '闸门一:读取 SKU 单价并校验上限', '设置数量、读回并执行闸门二', '确认页执行闸门三与控件唯一校验', '申请服务端提交围栏', '首次许可后持久化并内部点击一次', '调和同一提交结果'] }; const scenarios = { not_ready: { tone:'critical', title:'web 服务不可达', text:'未开始轮询。检查本地网络和服务地址;已完成的本地证据会保留待补传。', web:'bad', device:'ok', version:'ok', leg:'未就绪', legTone:'critical', action:'开始轮询', enabled:false, reason:'web 服务未就绪,不能领取任务。', session:'未启动', step:'连接检查', kind:'idle', active:-1, countdown:'—', fail:'1 / 3' }, version_mismatch: { tone:'critical', title:'拼多多版本与证据不一致', text:'已 fail closed:停止领取真机任务。下一步是由人重新取证并记录 App 版本,不能沿用旧判据。', web:'ok', device:'ok', version:'bad', leg:'版本失配', legTone:'critical', action:'开始轮询', enabled:false, reason:'App 版本失配;必须先完成本项目真机取证。', session:'已停止', step:'等待重新取证', kind:'idle', active:-1, countdown:'—', fail:'0 / 3' }, idle: { tone:'success', title:'设备已就绪(模拟)', text:'三项就绪条件在本场景中均为模拟通过,可以演示定时轮询;不会连接真实设备。', web:'ok', device:'ok', version:'ok', leg:'空闲', legTone:'success', action:'开始轮询', enabled:true, reason:'点击只切换原型状态,不发起网络请求。', session:'未启动', step:'等待开始', kind:'idle', active:-1, countdown:'—', fail:'0 / 3' }, polling: { tone:'info', title:'正在轮询,暂无待领任务', text:'无任务是正常空状态,不计入失败;将按配置间隔继续检查。', web:'ok', device:'ok', version:'ok', leg:'轮询中', legTone:'info', action:'停止轮询', enabled:true, reason:'停止后不再领取新任务。', session:'运行中', step:'等待下一轮', kind:'idle', active:-1, countdown:'00:12', fail:'0 / 3', count:2 }, - trial: { tone:'info', title:'第一趟试选正在执行', text:'只勾选规格、读价和截图;不会进入订单确认流程,完成后必须退出商品页释放手机。', web:'ok', device:'ok', version:'ok', leg:'TRIAL · 第一趟试选', legTone:'info', action:'真机步骤执行中', enabled:false, reason:'真机步骤期间禁用硬取消和关闭,避免留下无法判定的中间态。', session:'运行中', step:'读取规格面板单价', kind:'trial', active:2, countdown:'暂停', fail:'0 / 3', lease:'04:18' }, - trial_done: { tone:'success', title:'试选已回传,手机已释放', text:'规格与价格证据已保存(模拟);任务等待管理员确认,本设备继续下一轮。', web:'ok', device:'ok', version:'ok', leg:'TRIAL · 已完成', legTone:'success', action:'停止轮询', enabled:true, reason:'手机已退出商品页,可以安全停止轮询。', session:'运行中', step:'等待下一轮', kind:'trial', active:4, countdown:'00:09', fail:'0 / 3', count:3 }, - dry_run: { tone:'info', title:'DRY-RUN · 只读演练', text:'只到订单确认页读取非敏感摘要并验证控件唯一;不会提交订单,也不会消费授权。', web:'ok', device:'ok', version:'ok', leg:'DRY-RUN · 不会提交', legTone:'info', action:'只读演练进行中', enabled:false, reason:'演练中不提供提交动作;真机步骤期间受控关闭。', session:'运行中', step:'读取订单确认页', kind:'dry', active:2, countdown:'暂停', fail:'0 / 3', lease:'03:42' }, - dry_run_ready: { tone:'success', title:'只读演练已完成', text:'演练证据已回传(模拟)并退出确认页。旧演练不能替代真实第二趟的当前页面事实。', web:'ok', device:'ok', version:'ok', leg:'DRY-RUN · READY', legTone:'success', action:'停止轮询', enabled:true, reason:'等待服务端安排真实第二趟;不会由用户直接触发提交。', session:'运行中', step:'等待真实第二趟', kind:'dry', active:4, countdown:'00:16', fail:'0 / 3', count:4 }, - order_prefence: { tone:'caution', title:'真实第二趟 · 围栏前校验', text:'正在重新验证授权、闸门二、数量、闸门三和控件唯一。此时尚无点击许可。', web:'ok', device:'ok', version:'ok', leg:'ORDER · 围栏前', legTone:'caution', action:'安全校验进行中', enabled:false, reason:'没有服务端围栏许可,绝不能点击提交订单。', session:'运行中', step:'闸门三:核对实付款', kind:'order', active:2, countdown:'暂停', fail:'0 / 3', lease:'02:58' }, - fencing: { tone:'caution', title:'正在申请服务端提交围栏', text:'等待明确的 click_permitted。超时、冲突、网络失败或响应不明都不会点击。', web:'ok', device:'ok', version:'ok', leg:'ORDER · 申请围栏', legTone:'caution', action:'等待围栏响应', enabled:false, reason:'申请中禁止取消、关闭或重复申请。', session:'运行中', step:'申请唯一提交围栏', kind:'order', active:3, countdown:'暂停', fail:'0 / 3', lease:'02:31' }, - fence_failed: { tone:'critical', title:'未获得点击许可', text:'围栏申请失败或响应不明:没有点击提交订单。请按幂等键核查服务端记录,不能直接重试。', web:'bad', device:'ok', version:'ok', leg:'ORDER · 未获许可', legTone:'critical', action:'打开围栏核查说明', enabled:true, safeAction:'review', reason:'只核查围栏状态,不会触发提交。', session:'已停止', step:'核查幂等键', kind:'order', active:3, countdown:'—', fail:'1 / 3', submission:'围栏状态未知 · 幂等键 DEMO-FENCE-042' }, - fenced: { tone:'caution', title:'围栏已建立,只核对同一提交', text:'submission_id 已固定。内部流程至多点击一次;用户界面不提供提交、重领、放弃或重试入口。', web:'ok', device:'ok', version:'ok', leg:'ORDER · 已围栏', legTone:'caution', action:'打开同一提交核查说明', enabled:true, safeAction:'review', reason:'只能查看与调和同一提交记录。', session:'受控执行', step:'调和订单结果', kind:'order', active:5, countdown:'暂停', fail:'0 / 3', lease:'01:49', submission:'SUB-DEMO-0042 · FENCED' }, - waiting_payment: { tone:'success', title:'已明确创建待付款订单', text:'自动化已停止。系统不会付款;请人在拼多多核对后自行付款,再到采购服务标记。', web:'ok', device:'ok', version:'ok', leg:'WAITING_PAYMENT', legTone:'success', action:'查看待付款说明', enabled:true, safeAction:'paymentInfo', reason:'仅显示人工收口说明,不执行支付。', session:'已停止', step:'等待人工付款', kind:'order', active:6, countdown:'—', fail:'0 / 3', count:5, submission:'SUB-DEMO-0042 · SUBMITTED' }, - uncertain: { tone:'critical', title:'订单可能已创建,结果不明确', text:'保持金额预留和永久围栏,只核查 SUB-DEMO-0042;禁止重新领取、放弃授权或再次点击。', web:'ok', device:'ok', version:'ok', leg:'RECONCILIATION_REQUIRED', legTone:'critical', action:'打开人工核查说明', enabled:true, safeAction:'review', reason:'核查不会发起新的下单。可另行导出假证据摘要。', session:'已停止', step:'人工核查同一提交', kind:'order', active:5, countdown:'—', fail:'0 / 3', submission:'SUB-DEMO-0042 · 结果不明确' }, - 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:'未建立' }, + claimed: { tone:'info', title:'已领取管理员授权的采购任务', text:'任务字段与最高总价已锁定;当前只准备打开目标商品,尚未建立提交围栏。', web:'ok', device:'ok', version:'ok', leg:'单趟 · 已领取', legTone:'info', action:'真机步骤执行中', enabled:false, reason:'当前任务已开始,停止轮询只应阻止下一次领取,不能硬取消本任务。', session:'运行中', step:'核对授权与商品', kind:'single', active:0, countdown:'暂停', fail:'0 / 3', lease:'04:48' }, + selecting: { tone:'info', title:'正在精确选择颜色与尺码', text:'只在已取证维度容器内精确唯一匹配并读回;缺失、重复或版本变化立即停止。', web:'ok', device:'ok', version:'ok', leg:'单趟 · 选择规格', legTone:'info', action:'真机步骤执行中', enabled:false, reason:'当前受控步骤不能被硬取消。', session:'运行中', step:'精确选择规格', kind:'single', active:1, countdown:'暂停', fail:'0 / 3', lease:'04:18' }, + gate1: { tone:'info', title:'闸门一:读取 SKU 单价', text:'当前规格单价必须唯一可读,单价乘数量不得超过管理员授权的最高总价。', web:'ok', device:'ok', version:'ok', leg:'单趟 · 闸门一', legTone:'info', action:'安全校验进行中', enabled:false, reason:'价格不可读或超上限时立即停止。', session:'运行中', step:'闸门一:读取 SKU 单价', kind:'single', active:2, countdown:'暂停', fail:'0 / 3', lease:'03:58' }, + quantity_gate2: { tone:'info', title:'数量复核与闸门二', text:'数量必须读回为 2;目标规格不能漂移,第二次单价必须等于闸门一 CNY 32.50。', web:'ok', device:'ok', version:'ok', leg:'单趟 · 闸门二', legTone:'info', action:'安全校验进行中', enabled:false, reason:'数量、规格或价格变化都停止。', session:'运行中', step:'数量读回与闸门二', kind:'single', active:3, countdown:'暂停', fail:'0 / 3', lease:'03:31' }, + confirm_gate3: { tone:'caution', title:'确认页与闸门三', text:'正在核对规格、数量、应付总额与提交控件唯一;此时没有点击许可。', web:'ok', device:'ok', version:'ok', leg:'单趟 · 闸门三', legTone:'caution', action:'安全校验进行中', enabled:false, reason:'没有服务端围栏许可,绝不能点击提交订单。', session:'运行中', step:'闸门三:核对应付总额', kind:'single', active:4, countdown:'暂停', fail:'0 / 3', lease:'02:58' }, + fencing: { tone:'caution', title:'正在申请服务端提交围栏', text:'等待首次明确的 click_permitted。超时、冲突、网络失败或响应不明都不会点击。', web:'ok', device:'ok', version:'ok', leg:'单趟 · 申请围栏', legTone:'caution', action:'等待围栏响应', enabled:false, reason:'申请中禁止取消、关闭或换 key 重复申请。', session:'运行中', step:'申请唯一提交围栏', kind:'single', active:5, countdown:'暂停', fail:'0 / 3', lease:'02:31' }, + fence_failed: { tone:'critical', title:'未获得首次明确点击许可', text:'围栏失败或响应不明:没有点击提交订单。只核查同一幂等键,不能直接重试。', web:'bad', device:'ok', version:'ok', leg:'单趟 · 未获许可', legTone:'critical', action:'打开围栏核查说明', enabled:true, safeAction:'review', reason:'只核查围栏状态,不触发提交。', session:'已停止', step:'核查幂等键', kind:'single', active:5, countdown:'—', fail:'1 / 3', submission:'围栏状态未知 · 幂等键 DEMO-FENCE-042' }, + fenced: { tone:'caution', title:'围栏已建立,只核对同一提交', text:'submission_id 已固定。内部流程至多点击一次;界面不提供提交、重领、放弃或重试。', web:'ok', device:'ok', version:'ok', leg:'单趟 · 已围栏', legTone:'caution', action:'打开同一提交核查说明', enabled:true, safeAction:'review', reason:'只能查看与调和同一提交记录。', session:'受控执行', step:'调和订单结果', kind:'single', active:7, countdown:'暂停', fail:'0 / 3', lease:'01:49', submission:'SUB-DEMO-0042 · FENCED' }, + waiting_payment: { tone:'success', title:'已明确创建待付款订单', text:'自动化已停止。系统不会付款;请人在拼多多核对后自行付款,再到采购服务标记。', web:'ok', device:'ok', version:'ok', leg:'WAITING_PAYMENT', legTone:'success', action:'查看待付款说明', enabled:true, safeAction:'paymentInfo', reason:'仅显示人工收口说明,不执行支付。', session:'已停止', step:'等待人工付款', kind:'single', active:8, countdown:'—', fail:'0 / 3', count:5, submission:'SUB-DEMO-0042 · SUBMITTED' }, + uncertain: { tone:'critical', title:'订单可能已创建,结果不明确', text:'保持金额预留和永久围栏,只核查 SUB-DEMO-0042;禁止重新领取、释放授权或再次点击。', web:'ok', device:'ok', version:'ok', leg:'RECONCILIATION_REQUIRED', legTone:'critical', action:'打开人工核查说明', enabled:true, safeAction:'review', reason:'核查不会发起新的下单。可另行导出假证据摘要。', session:'已停止', step:'人工核查同一提交', kind:'single', active:7, countdown:'—', fail:'0 / 3', submission:'SUB-DEMO-0042 · 结果不明确' }, + security: { tone:'critical', title:'检测到安全校验,已停止', text:'不绕过验证码、风控、人脸或短信。若已围栏,只调和同一提交;围栏前转人工。', web:'ok', device:'ok', version:'ok', leg:'待人工 · 安全校验', legTone:'critical', action:'打开人工核查说明', enabled:true, safeAction:'review', reason:'只显示核查说明,不提供绕过或继续。', session:'已停止', step:'等待人工处理', kind:'single', active:7, 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:'single', active:7, 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:'导出只生成本地假文本,不含完整 XML、页面全文或敏感字段。', session:'继续轮询其他任务', step:'当前任务已移交', kind:'single', active:3, 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 } }; const sessionControls = { not_ready:{label:'开始轮询',enabled:false,reason:'web 服务未就绪,不能领取任务。'}, version_mismatch:{label:'开始轮询',enabled:false,reason:'App 版本失配,必须先完成本项目真机取证。'}, - idle:{label:'开始轮询',enabled:true,reason:'只启动普通轮询,不会直接执行下单。'}, + 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:'真实第二趟校验中,禁止硬停止。'}, + claimed:{label:'停止轮询',enabled:false,reason:'当前任务已领取;停止只应阻止下次领取,不能硬取消本任务。'}, + selecting:{label:'停止轮询',enabled:false,reason:'受控真机步骤执行中,禁止硬停止。'}, + gate1:{label:'停止轮询',enabled:false,reason:'价格闸门执行中,禁止硬停止。'}, + quantity_gate2:{label:'停止轮询',enabled:false,reason:'数量与闸门二执行中,禁止硬停止。'}, + confirm_gate3:{label:'停止轮询',enabled:false,reason:'确认页只允许安全校验,禁止硬停止或额外点击。'}, fencing:{label:'停止轮询',enabled:false,reason:'围栏申请中,禁止关闭、取消或重复申请。'}, fence_failed:{label:'开始轮询',enabled:false,reason:'先核查围栏状态;不能用重新轮询绕过不明确结果。'}, fenced:{label:'停止轮询',enabled:false,reason:'已建立围栏,只能调和同一提交。'}, @@ -836,10 +834,10 @@ $('task-id').textContent = hasTask ? task.id : '—'; $('lease').textContent = s.lease || '—'; $('product').textContent = hasTask ? task.product : '暂无商品;轮询会在安全就绪后领取一条任务'; $('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 : '—'; + $('gate1-price').textContent = hasTask && ['gate1','quantity_gate2','confirm_gate3','fencing','fence_failed','fenced','waiting_payment','uncertain','security','payment_handoff','manual'].includes($('scenario').value) ? task.gate1Price : '—'; $('price-cap').textContent = hasTask ? task.cap : '—'; $('submission').textContent = s.submission || '未建立'; $('preview-empty').hidden = hasTask; $('product-preview-image').hidden = !hasTask; - $('preview-source').textContent = 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); @@ -888,7 +886,7 @@ 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); + const unsafe = ['claimed','selecting','gate1','quantity_gate2','confirm_gate3','fencing','fenced'].includes($('scenario').value); showDialog(unsafe ? '当前不能直接关闭' : '关闭窗口协议', unsafe ? '真机或围栏流程处于受控阶段。原生实现需先完成安全停靠或保持同一 submission 调和,不能让关闭看起来等于“可以重来”。' : '原生实现关闭窗口后立即停止普通轮询,不留后台进程。此 HTML 只演示规则,不会关闭浏览器。'); }); $('dialog-close').addEventListener('click', () => $('info-dialog').close()); diff --git a/docs/design/web-task-create.html b/docs/design/web-task-create.html index 7391375..149eea9 100644 --- a/docs/design/web-task-create.html +++ b/docs/design/web-task-create.html @@ -203,11 +203,11 @@

创建后发生什么

  1. 任务先保存为「待开始」。
  2. -
  3. 在任务表格勾选后统一点击「开始试选」。
  4. -
  5. 第一趟只试选、读价、截图,然后退出商品。
  6. -
  7. 你看过证据后再决定是否授权。
  8. +
  9. 在任务表格勾选后点击「开始采购(只创建待付款订单)」。
  10. +
  11. 该点击锁定商品、规格、数量和最高总价,并授权采购工具执行。
  12. +
  13. 采购工具同一趟校验规格、价格和数量后,只创建待付款订单。
-

系统不会自动付款。

+

创建任务本身不授权;系统任何时候都不会自动付款。

原型状态

@@ -310,7 +310,7 @@ setReady(); form.hidden = true; status.className = 'status success show'; - status.innerHTML = '

任务已创建

任务编号 T-20260803-019,当前为「待开始」。请回到任务表格勾选后点击“开始试选”;系统尚未进行任何拼多多操作。

查看任务工作台'; + status.innerHTML = '

任务已创建

任务编号 T-20260803-019,当前为「待开始」。请回到任务表格勾选后点击“开始采购(只创建待付款订单)”;创建本身尚未授权,也没有进行任何拼多多操作。

查看任务工作台'; status.focus?.(); } diff --git a/docs/design/web-task-detail.html b/docs/design/web-task-detail.html index d0b79a5..e7b25b0 100644 --- a/docs/design/web-task-detail.html +++ b/docs/design/web-task-detail.html @@ -2,333 +2,98 @@ - - - 任务详情 · 采购服务原型 + + 采购服务 · 任务详情原型 - -
PROTOTYPE - 仅供枚举交互,非实现依据
-
- 采购服务 - - -
- + +
PROTOTYPE - 仅供枚举交互,非实现依据
+
采购服务
- -
-

8 月仓库工作服补货

任务 T-20260803-018 · 假数据

- 等待确认 -
-
只下单,不付款系统最多创建待付款订单。付款始终由人在拼多多核对后完成;待付款状态不是成功。
+ +

8 月仓库工作服补货

任务 T-20260803-018 · 假数据

待开始
+
开始采购会授权创建待付款订单;系统不会付款。 任务执行时实时通过三道价格闸门。围栏后只能调和同一提交,禁止再次点击。
-
-
+
+
-
-

机器选对了吗?

确认前必须查看实际勾选结果、价格和规格面板证据。

-
-
-
可以授权机器所选规格与要求一致,合计未超过资金上限。
- - - - - - - -
核对项你要的机器选到结论
颜色分类白色白色符合
尺码XLXL符合
金额总额不超过 ¥80.00¥32.50 × 2 = ¥65.00未超上限
-
- - 规格面板截图假数据占位用于确认信息架构,不代表真实拼多多页面结构或字段位置。 - - 假数据证据占位 · 不代表真实 App 页面 - 颜色分类:白色(假) - 尺码:XL(假) - 规格面板读价:¥32.50(假) - 真实可读字段需等待 T-103 真机取证后复核 - -
规格面板证据 · 假数据 · 未包含地址、手机号或付款信息
-
- -
-
+
+

待开始

任务已保存,但尚未授权采购工具领取或创建订单。

+
创建不等于授权请返回任务列表勾选任务,并点击“开始采购(只创建待付款订单)”。
商品
goods_id 937122477375
颜色 / 尺码
黑色CHA(纯棉) / M(建议100-115)
数量
2 件
最高总价
¥30.00
-
-
-

确认退回,不买?

任务将结束为已取消,不会签发下单授权,也不会再自动执行。

-

放弃当前授权?

当前确认尚未建立提交围栏,可以安全放弃。

放弃后必须重新跑第一趟取新价,不能复用旧授权或旧截图。

- +

确认关闭旧授权并重置?

当前没有提交围栏。旧授权会永久关闭,任务回到待开始;必须由管理员重新点击开始采购。

diff --git a/docs/design/web-task-workbench.html b/docs/design/web-task-workbench.html index 875132a..2826e3d 100644 --- a/docs/design/web-task-workbench.html +++ b/docs/design/web-task-workbench.html @@ -191,8 +191,8 @@
-

采购任务

创建后先保存为待开始;勾选后统一进入第一趟试选队列。

-

只开始试选,不会下单。 下单必须在试选完成并经人工确认后另行授权,付款始终由人完成。

+

采购任务

创建后先保存为待开始;勾选并开始采购后,采购工具按授权逐条执行。

+

开始采购会授权创建待付款订单。 系统校验规格、数量和金额后最多提交一次,任何时候都不会自动付款。

@@ -213,13 +213,13 @@ @@ -268,7 +268,7 @@

采购要求

采购结果

当前状态与下一步

-

执行证据

假数据证据占位
真实可读字段必须等待 T-103 真机取证;不展示地址、手机号或付款信息。
+

执行证据

内部截图证据占位
真实字段等待真机取证;完整 XML 与支付凭据不会进入采购服务。
@@ -278,7 +278,7 @@ 'use strict'; const tasks = [ { id:'T-20260803-023', title:'仓库雨衣补货', productUrl:'https://mobile.yangkeduo.invalid/goods.html?goods_id=900000023', color:'荧光黄', size:'XXL', maxPrice:'96.00', quantity:3, status:'DRAFT', createdAt:'2026-08-03T14:42:00+08:00', result:null }, - { id:'T-20260803-022', title:'8 月仓库工作服补货', productUrl:'https://mobile.yangkeduo.invalid/goods.html?goods_id=900000022', color:'白色', size:'XL', maxPrice:'80.00', quantity:2, status:'WAITING_CONFIRMATION', createdAt:'2026-08-03T13:18:00+08:00', result:{ phase:'试选', color:'白色', size:'XL', unitPrice:'32.50', quantity:2 } }, + { id:'T-20260803-022', title:'8 月仓库工作服补货', productUrl:'https://mobile.yangkeduo.invalid/goods.html?goods_id=900000022', color:'白色', size:'XL', maxPrice:'80.00', quantity:2, status:'ORDERING', createdAt:'2026-08-03T13:18:00+08:00', result:{ phase:'采购执行', color:'白色', size:'XL', unitPrice:'32.50', quantity:2 } }, { id:'T-20260803-021', title:'办公室收纳盒', productUrl:'https://mobile.yangkeduo.invalid/goods.html?goods_id=900000021', color:'透明', size:'L', maxPrice:'60.00', quantity:2, status:'RECONCILIATION_REQUIRED', createdAt:'2026-08-03T11:36:00+08:00', result:{ phase:'下单待核查', color:'透明', size:'L', unitPrice:'24.00', quantity:2 } }, { id:'T-20260803-020', title:'仓库防滑手套', productUrl:'https://mobile.yangkeduo.invalid/goods.html?goods_id=900000020', color:'黑色', size:'均码', maxPrice:'64.00', quantity:5, status:'PENDING', createdAt:'2026-08-03T10:45:00+08:00', result:null }, { id:'T-20260801-009', title:'标签打印纸补货', productUrl:'https://mobile.yangkeduo.invalid/goods.html?goods_id=900000009', color:'白色', size:'50×30', maxPrice:'50.00', quantity:1, status:'WAITING_PAYMENT', createdAt:'2026-08-01T16:12:00+08:00', result:{ phase:'已下单', color:'白色', size:'50×30', unitPrice:'46.00', quantity:1 } }, @@ -286,9 +286,10 @@ ]; const statusMeta = { - DRAFT:{ label:'待开始', tone:'neutral', next:'勾选后点击“开始试选”,任务才会进入设备领取队列。' }, - PENDING:{ label:'待领取', tone:'', next:'已进入第一趟试选队列,等待桌面端轮询领取。' }, - WAITING_CONFIRMATION:{ label:'等待确认', tone:'', next:'查看规格面板证据,确认机器选对后才可签发下单授权。' }, + DRAFT:{ label:'待开始', tone:'neutral', next:'勾选后点击“开始采购(只创建待付款订单)”,才会签发一次性授权。' }, + PENDING:{ label:'已授权待领取', tone:'', next:'管理员已授权创建待付款订单,等待采购工具领取;系统不会付款。' }, + ORDERING:{ label:'采购执行中', tone:'', next:'采购工具正在同一趟校验规格、价格和数量;无需中途确认。' }, + NEEDS_MANUAL:{ label:'待人工处理', tone:'warning', next:'围栏前执行已停止。核查原因后可重置为待开始或取消。' }, RECONCILIATION_REQUIRED:{ label:'结果待核查', tone:'warning', next:'订单可能已创建。只核查同一提交记录,不得重新提交。' }, WAITING_PAYMENT:{ label:'待付款', tone:'warning', next:'人在拼多多核对商品、规格、数量和金额后手工付款;系统不会付款。' }, SUCCEEDED:{ label:'已完成', tone:'success', next:'已由人工付款并标记完成,可查看执行证据与审计记录。' } @@ -303,6 +304,7 @@ const filterForm = document.querySelector('#filter-form'); const selectionMode = document.querySelector('#selection-mode'); const selectionCount = document.querySelector('#selection-count'); + const selectionCap = document.querySelector('#selection-cap'); const selectAll = document.querySelector('#select-all'); const states = { loading:document.querySelector('#loading-state'), empty:document.querySelector('#empty-state'), error:document.querySelector('#error-state') }; const selectedIds = new Set(); @@ -325,6 +327,12 @@ return `${whole}.${fraction}`; } + function moneyToCents(value) { + const normalized = normalizeMoney(value); + const [whole, fraction] = normalized.split('.'); + return Number.parseInt(whole, 10) * 100 + Number.parseInt(fraction, 10); + } + function formatDate(value) { return new Intl.DateTimeFormat('zh-CN',{ year:'numeric', month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit', hour12:false }).format(new Date(value)); } @@ -375,6 +383,8 @@ filterForm.hidden = count > 0; selectionMode.hidden = count === 0; selectionCount.textContent = `已选择 ${count} 条待开始任务`; + const capInCents = tasks.filter((task) => selectedIds.has(task.id)).reduce((sum, task) => sum + moneyToCents(task.maxPrice), 0); + selectionCap.textContent = `最高总额合计 ¥${(capInCents / 100).toFixed(2)}`; const selectableVisible = visibleTasks().filter((task) => task.status === 'DRAFT'); const selectedVisible = selectableVisible.filter((task) => selectedIds.has(task.id)); selectAll.checked = selectableVisible.length > 0 && selectedVisible.length === selectableVisible.length; @@ -438,12 +448,12 @@ }); document.querySelector('#clear-selection').addEventListener('click', () => { selectedIds.clear(); renderRows(); keyword.focus(); }); - document.querySelector('#start-trial').addEventListener('click', () => { + document.querySelector('#start-purchase').addEventListener('click', () => { const ids = [...selectedIds]; tasks.forEach((task) => { if (selectedIds.has(task.id) && task.status === 'DRAFT') task.status = 'PENDING'; }); selectedIds.clear(); renderRows(); - showToast(`已将 ${ids.length} 条任务加入第一趟试选队列。没有签发下单授权,也不会付款。`); + showToast(`已授权 ${ids.length} 条任务创建待付款订单。采购工具将逐条执行,系统不会付款。`); }); function isInteractive(target) { return Boolean(target.closest('a,button,input,label,select,textarea,summary')); } @@ -548,7 +558,7 @@ drawerId.textContent = `${task.id} · ${meta.label}`; drawerTitle.textContent = task.title; drawerRequirements.innerHTML = `
颜色
${escapeHtml(task.color)}
尺码
${escapeHtml(task.size)}
价格上限
¥${escapeHtml(task.maxPrice)}
数量
${task.quantity} 件
状态
${escapeHtml(meta.label)}
创建时间
${escapeHtml(formatDate(task.createdAt))}
`; - drawerResult.innerHTML = task.result ? `

${formatResult(task)}

展示值来自结构化颜色、尺码、单价和数量;“|”只用于页面格式化。

` : '

尚无采购结果。任务未完成试选或下单,不用其他数据填充。

'; + drawerResult.innerHTML = task.result ? `

${formatResult(task)}

展示值来自结构化颜色、尺码、单价和数量;“|”只用于页面格式化。

` : '

尚无采购结果。任务未开始或仍在执行,不用其他数据填充。

'; drawerNext.textContent = meta.next; fullDetailLink.href = `web-task-detail.html#${encodeURIComponent(task.id)}`; } diff --git a/docs/routes.md b/docs/routes.md index cea05e4..d742a5d 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -1,157 +1,147 @@ # 路由与页面结构 -> 本文约定采购服务(网页端,`admin/`)页面路由、页面职责和组件归属,以及采购工具 -> (桌面端,`client/`)的界面结构。 -> 具体交互行为以[交互清单](08-interaction-checklist.md)为准,接口形状以 [API 合约](api.md) 为准。 +> 采购服务使用服务端渲染;采购工具使用固定 tab 的 Windows 桌面外壳。交互细节以 +> [08-interaction-checklist.md](08-interaction-checklist.md) 为准。 ## 一、采购服务页面路由 -网页端用户可见产品名统一为“采购服务”。`cmbuyer` 只作为仓库和系统内部标识,不出现在 -网页标题、页头品牌或无障碍名称中;桌面端仍使用独立名称“采购工具”。 - -| 路由 | 页面 | 职责 | 用户故事 | 交互 | -| --- | --- | --- | --- | --- | -| `/login` | 登录 | 建立管理会话 | US-007 | IX-001 | -| `/tasks` | 采购任务 | 传统表格查询、创建弹窗、批量开始试选、进入详情抽屉 | US-001、US-002、US-010 | IX-002、IX-003、IX-004、IX-012 | -| `/tasks/new` | 手工建单直达页 | 复用创建表单,作为无脚本和直接访问的完整页兜底 | US-001 | IX-002 | -| `/tasks/{id}` | 任务详情 | 从列表进入时显示右侧详情抽屉;直接访问时显示完整页;承载试选确认、授权与待付款核对 | US-002、US-004、US-005 | IX-004、IX-005、IX-006 | - -> `/tasks/import`(Excel)与 `/freight*`(ERP)随 F-002 / F-003 推迟到 V2。 - -登录后默认进入 `/tasks`。未登录访问受保护页面时跳转 `/login` 并携带站内返回路径; -**只接受 `/tasks` 及其子路径**,拒绝绝对 URL、`//` 和反斜杠。 - -## 二、采购服务页面职责 - -### 采购任务 `/tasks` - -采用采购人员熟悉的传统表格,默认按创建时间倒序。固定列为:选择、标题、颜色、尺码、 -价格上限、数量、采购结果、状态、创建时间;不设置操作列。 - -- 标题链接在新标签打开拼多多商品页;标题下方任务编号链接打开内部任务详情。 -- “采购结果”由结构化字段在展示层格式化为“阶段:颜色 | 尺码 | 单价 | 数量”;未产生 - 试选或下单结果时显示 `—`,不得把试选写成已下单。 -- 双击行非控件区域,或聚焦行后按 Enter,打开 `/tasks/{id}` 的右侧详情抽屉。关闭抽屉、 - 按 Esc 或浏览器返回后,保留筛选、选择、滚动位置并把焦点还给原行。 -- 直接访问 `/tasks/{id}` 或在抽屉中选择“在完整页面打开”时,使用完整详情页;关键路由可 - 深链接,不能只有无法复制地址的弹层状态。 -- 第一工具行:导入(MVP 禁用占位并说明原因)、创建。创建打开模态表单;保存后任务以 - `DRAFT` 状态出现在第一行。`/tasks/new` 保留为同表单的直达兜底。 -- 第二工具行默认显示标题关键词、筛选、清除;状态和创建时间放在默认折叠的“更多条件”中, - 保留 F-004 查询范围而不挤占常用操作。勾选 `DRAFT` 后切换成上下文批量栏,显示已选数量、 - 开始试选、清除选择;全选只覆盖当前筛选结果中的 `DRAFT`。 -- “开始试选”把所选任务原子转为 `PENDING`,只进入第一趟试选队列;不签发授权、不创建 - 订单、不付款。其他状态复选框禁用并说明原因。 -- 窄屏允许表格容器内部横向滚动,但页面本身不得横向溢出;选择列与标题列保持可见。 - -### 任务详情 `/tasks/{id}` - -按任务当前状态呈现不同主区块,同一时刻只出现一个主动作: - -| 状态 | 主区块 | -| --- | --- | -| `WAITING_CONFIRMATION` | **试选确认卡** + 确认 / 退回 | -| `AUTHORIZED` / 未围栏的 `ORDERING` | 授权摘要 + **放弃授权**入口 | -| 已围栏的 `ORDERING` / `RECONCILIATION_REQUIRED` | 提交围栏摘要 + 「订单可能已创建」提示 + 人工核查入口;**无重试 / 放弃按钮** | -| `WAITING_PAYMENT` | 待付款核对卡(订单截图、商品、规格、数量、授权金额)+ 标记完成 | -| `NEEDS_MANUAL` | 原因说明 + 处理入口 | -| 终态 | 结果摘要 + 执行证据 | - -**试选确认卡**是 MVP 的核心交互。人只回答一个问题:**机器选对了吗**。 - -```text -你要的: 白色 · XL · 2 件 · 上限 ¥80 -机器选到: 白色 · XL ✓ 一致 - 单价 ¥32.50 × 2 = ¥65.00 ✓ 没超 - [规格面板截图] - 商品标题:2026夏季新款纯棉圆领短袖T恤… - - [ 确认下单(不付款) ] [ 退回,不买 ] -``` - -- 这是**轻量确认,不是对照台**——只有一个商品,不需要并排比较多个候选。 - 多候选对照台随 B 路径推迟到 V2。 -- 不需要「选择理由 / 拒绝理由」下拉,保留一个可选备注即可。 -- 机器选到的值与需求不一致时(✗),确认按钮**默认禁用**,需先退回或转人工。 -- **不提供列表页一键确认。** 确认前必须看过截图,这道闸不能省。 - -### 授权卡住时的出口 - -`AUTHORIZED` / 未建立提交围栏的 `ORDERING` 必须提供「放弃授权」入口;放弃后进入 -`PENDING_RETRIAL`,重新跑第一趟。围栏建立后不得放弃,页面改为提供「进入人工核查」入口, -并明确禁止再次提交。**每个状态都必须给出安全且可执行的下一步**——这是验收项。 - -### 建单页 - -`/tasks` 的创建弹窗与 `/tasks/new` 直达页复用同一表单:任务名称、拼多多链接、颜色分类、 -尺码、数量、价格上限。链接无法解析出 `goods_id` 时明确报错并保留已填内容;保存成功后 -任务为 `DRAFT`,尚未进入设备领取队列。 - -## 三、采购工具界面结构 - -桌面端产品名为“采购工具”,不是网页;使用顶部固定两页签,不做多级导航,默认打开采购执行: - -| 页签 | 职责 | 用户故事 | 交互 | +| 方法 | 路径 | 页面 / 动作 | 身份 | | --- | --- | --- | --- | -| 采购执行 | 连接与会话状态、当前任务与可选图片、滚动日志、执行记录 | US-003、US-008 | IX-007、IX-008 | -| 配置 | 设备档案、ADB 路径与 serial、常用超时参数、连接检查 | US-003 | IX-007 | +| `GET` | `/login` | 登录页 | 匿名 | +| `POST` | `/login` | 建立会话 | 匿名 + CSRF | +| `POST` | `/logout` | 退出 | 管理员 + CSRF | +| `GET` | `/tasks` | 采购任务表格、筛选、批量选择和创建入口 | 管理员 | +| `GET` | `/tasks/new` | 无 JS 时的创建表单;有 JS 时装入对话框 | 管理员 | +| `POST` | `/tasks` | 创建 `DRAFT` | 管理员 + CSRF | +| `POST` | `/tasks/start-purchases` | 批量开始采购并签发一次性授权 | 管理员 + CSRF | +| `GET` | `/tasks/{id}` | 完整任务详情;也作为列表抽屉的可复制 URL | 管理员 | +| `POST` | `/tasks/{id}/reset-to-draft` | 围栏前人工处理后回待开始 | 管理员 + CSRF | +| `POST` | `/tasks/{id}/cancel` | 围栏前取消 | 管理员 + CSRF | +| `POST` | `/tasks/{id}/mark-paid` | 记录人工已付款并完成 | 管理员 + CSRF | +| `POST` | `/order-submissions/{sid}/reconcile` | 围栏后调和同一提交 | 管理员 + CSRF | +| `GET` | `/evidence/{asset_id}` | 受保护内部截图 | 管理员;不缓存 | -### 采购执行页 +设备 JSON API 不属于页面路由,见 [api.md](api.md)。 -- 顶部状态区:web、设备、拼多多 App 实际 / 已取证版本和会话状态;开始 / 停止轮询固定在 - 最右侧且只控制轮询会话。版本不一致时 fail closed 并提示重新取证。 -- 宽屏是主从工作区:左侧在“当前任务 + 滚动日志”和“执行记录详情”之间切换,右侧执行记录 - 保持可见;窄屏进入详情时暂时收起记录表,返回后恢复列表现场。 -- 当前任务左侧显示商品、规格、数量、**当前是第一趟还是第二趟**、步骤和剩余租约;右侧约 - 1/3 显示可选商品 / 证据预览。无可信图片时保持空态,有图必须标来源与采集时间。 -- 滚动日志标题区同时显示下次轮询、连续失败和本次完成,不再使用独立会话卡或底部控制条。 -- 执行记录只显示标题、状态两列,任务编号与时间放在标题次行,按采集时间倒序。单击选择; - 双击、Enter、可见“查看所选记录”或右键在左侧打开详情;详情态单击另一行直接更新详情。 -- 记录详情不是模态框:上部左侧显示原始文字、右侧显示图片证据,下部显示结构化采购结果; - 可见“返回当前任务”与 `Esc` 均能返回并恢复当前行焦点。加载失败、无图和记录不存在有明确 - 状态,图片缺失不使用其他来源凑合。 -- **待人工时整页显著变色并说明缺什么**,不要让执行员盯着一个静止画面猜。 -- 关闭窗口即停止轮询;连续失败达阈值自动停止并显示原因。 +## 二、采购任务工作台 `/tasks` -### 配置页 +页面标题:**采购服务**。 -- 设备档案必须**显式填写 serial**,不允许留空自动选——同一手机 USB + WiFi 同时在线时 - 自动选会失败(见[架构设计](04-architecture.md)第六节)。 -- 连接检查只做连接和确认拼多多已安装,**不打开商品、不选规格、不创建订单**。 -- 运行中冻结设备切换与参数保存。 +表格上方两行: -## 四、导航规则 +1. 第一行:导入(disabled 占位)、创建按钮;存在选择时显示上下文批量操作条。 +2. 第二行:标题关键词输入框(默认筛选)、其他可选筛选,右侧“筛选”“清除”。 -- 采购服务从路由化详情抽屉返回工作台时不重新加载列表;完整详情页返回时保留原筛选条件。 -- 采购工具启动时若服务端有本设备的活跃任务,优先恢复该任务,不允许直接领取下一条。 -- 第一趟试选完成后**必须退出商品页**再进入下一轮轮询,不停在规格面板等人。 -- 采购工具在真机步骤执行期间禁用硬取消和关闭窗口,避免留下无法判定的中间态。 -- 任何进入外部支付页的情形,采购工具立即停止并跳回待人工,**不提供「继续」按钮**。 -- dry-run 与真实下单必须使用不同的醒目标识;dry-run 不得出现可触发真实提交的控件。 -- 已建立提交围栏后,不提供重新领取、重新提交或放弃授权,只能恢复同一提交记录并调和。 +表格字段: -## 五、组件归属 +| 字段 | 行为 | +| --- | --- | +| checkbox | 只允许选择 `DRAFT`;表头全选当前筛选结果中的可选行 | +| 标题 | `` 指向 canonical 拼多多商品页;点击链接不触发行详情 | +| 颜色 | 任务目标颜色 | +| 尺码 | 任务目标尺码 | +| 价格 | 任务最高总价;执行后详情另列实际闸门金额 | +| 数量 | 正整数 | +| 采购结果 | 用 `颜色|尺码|价格|数量` 展示已执行摘要;无结果显示 `—` | +| 状态 | 中文状态徽标,不只靠颜色 | +| 创建时间 | 本地时区显示,数据按 UTC 保存 | -| 组件 | 归属 | 说明 | -| --- | --- | --- | -| `AppShell` | web 全局 | 导航、登录态、CSRF 注入 | -| `TaskTable` | 采购任务 | 任务表格、创建时间倒序、选择状态与结构化采购结果格式化 | -| `QueryBar` | 采购任务 | 标题关键词、筛选与清除 | -| `BulkTrialBar` | 采购任务 | `DRAFT` 批量选择与开始第一趟试选 | -| `CreateTaskDialog` | 采购任务 / 建单直达页 | 复用手工建单表单与字段错误 | -| `TaskDetailDrawer` | 采购任务 | `/tasks/{id}` 路由驱动的右侧详情容器与焦点恢复 | -| `SpecTrialCard` | 任务详情 | 试选确认卡(MVP 签名组件) | -| `AuthorizePanel` | 任务详情 | 确认 / 退回 / 放弃授权 | -| `PaymentCheckCard` | 任务详情 | 待付款核对与标记完成 | -| `DeviceStatusBar` | desk 采购执行页 | web、设备与 App 版本三项就绪状态 | -| `PollControls` | desk 采购执行页 | 顶部固定的轮询开关与会话状态;日志标题区承载倒计时、失败和完成计数 | -| `CurrentTaskPanel` | desk 采购执行页 | 当前任务文字、步骤与带来源的可选图片证据 | -| `ExecutionLog` | desk 采购执行页 | 最新在上的滚动日志与假诊断导出 | -| `ExecutionRecordTable` | desk 采购执行页 | 标题 / 状态两列、时间倒序和稳定记录 ID | -| `ExecutionRecordDetailPanel` | desk 采购执行页 | 左侧内联原始文字、图片证据、采购结果、读取状态与返回焦点 | +没有操作列。双击非控件区域或键盘 Enter 打开 `/tasks/{id}` 路由化详情抽屉;新 tab 直接访问同 URL +则显示完整详情页。关闭抽屉或浏览器返回恢复筛选、滚动和触发行焦点。 -## 六、原型 +### 批量开始采购 -低保真原型放 `docs/design/`,约定见 [`design/README.md`](design/README.md)。 -原型只回答「页面上有什么」,行为权威是[交互清单](08-interaction-checklist.md); -实现时按真实框架重写,**不复制原型代码**。 +选择 `DRAFT` 后,表格前的上下文操作条显示: + +- 已选条数; +- 所选最高总价合计; +- “将授权采购工具逐条创建待付款订单,系统不会付款”; +- 主按钮“开始采购(只创建待付款订单)”。 + +点击即为最终授权,不另弹“机器选对了吗”的同义确认框。整批全有或全无;成功更新 `PENDING`, +冲突时保持页面现场并要求刷新重选。 + +### 创建任务 + +字段:标题、拼多多商品链接、颜色分类、尺码、最高总价、数量。保存成功后关闭对话框,新任务插到 +首行且状态为待开始;创建本身不授权、不领取、不执行。 + +## 三、任务详情 `/tasks/{id}` + +详情按状态展示同一条单趟采购的事实,而不是审批流程: + +1. 任务要求:商品链接 / goods_id、颜色、尺码、数量、最高总价、版本。 +2. 开始采购授权:授权 id、授权人、锁定任务版本、创建/有效期、当前状态。 +3. 设备执行:attempt、设备、App 版本、步骤时间线和失败 code。 +4. 三道闸门:两次规格面板单价、规格/数量读回、确认页总额与判定。 +5. 内部截图:规格面板、确认页和结果页;只经受保护端点读取。 +6. 提交围栏:submission id、是否首次明确许可、唯一点击和调和记录。 +7. 待付款收口:明确写系统尚未付款;人工核对后记录完成。 + +状态动作: + +| 状态 | 页面动作 | +| --- | --- | +| `DRAFT` | 返回列表勾选并开始采购;可编辑/取消(按任务版本) | +| `PENDING` / `CLAIMED` / `ORDERING` | 只读进度;围栏前异常由人工处理,不中途确认规格 | +| `NEEDS_MANUAL` | 查看原因;确认没有围栏后重置为 DRAFT 或取消 | +| `RECONCILIATION_REQUIRED` | 只调和同一 submission;无重试、释放或重新授权 | +| `WAITING_PAYMENT` | 查看证据、人工付款、标记完成或转人工 | +| 终态 | 只读审计 | + +详情中不出现 `WAITING_CONFIRMATION`、“确认机器选对了吗”、“签发第二趟授权”或“重新试选”。 + +## 四、采购工具界面结构 + +应用名:**采购工具**。顶部固定 tab: + +1. **采购执行**(默认) +2. **配置** + +### 采购执行 tab + +- 顶部第一行:采购服务、ADB、拼多多版本、会话状态;其右侧是“开始轮询 / 停止轮询”。 +- 左上“当前任务”:左侧文字约 2/3,右侧商品图片约 1/3;空闲显示占位。 +- 左下“滚动日志”:占满剩余高度,显示阶段、固定 reason 和安全下一步。 +- 右侧“采购记录”:时间倒序表格,MVP 只显示标题和状态。 +- 双击或 Enter 记录:左侧原位切换到记录详情,不弹窗;上方文字/图片,下方执行结果。 +- Esc 或“返回当前任务”:恢复当前任务视图;不暂停执行、不释放围栏。 + +单趟状态:待领取、已领取、打开商品、选择规格、闸门一、数量复核、闸门二、确认页/闸门三、 +申请围栏、已发出唯一提交、待付款或待调和。桌面端没有让用户手工点击“提交订单”的按钮。 + +### 配置 tab + +- 采购服务 URL、设备 token(密码框,不回显完整值); +- ADB 路径、设备 serial、USB/WiFi 通道选择; +- 轮询间隔与连续失败停止阈值; +- 连接检查:服务、设备身份、拼多多安装和已取证版本。 + +配置凭据进入系统安全存储;日志和界面不显示完整 token。 + +## 五、导航和焦点规则 + +- Web 主导航 MVP 只有“采购任务”;logo 文案为“采购服务”。 +- 商品标题链接是外部导航;行详情是内部导航,两者事件相互隔离。 +- Web 路由抽屉与完整页共享数据和 URL;关闭恢复触发行焦点。 +- Desk tab 使用标准键盘关系;历史详情 Esc 返回当前任务,不关闭应用。 +- 真机执行、围栏和提交不受页面/视图切换影响;关闭窗口时若有活跃任务,提示只影响 UI/轮询, + 不把它解释为撤销服务端授权。 + +## 六、组件归属 + +| 组件 | 归属 | +| --- | --- | +| 会话 / CSRF / SSR 模板 | `admin/internal/transport/webui` | +| 任务 / 授权 / attempt / submission 用例 | `admin/internal/usecase` | +| SQLite / 证据存储 | `admin/internal/storage` | +| 设备 API | `admin/internal/transport/httpapi` | +| ADB / PDD 页面能力 | `client/src/cmbuyer_client/device`、`pdd` | +| 任务来源与结果 sink | `client/src/cmbuyer_client/core`、`remote` | +| PySide6 UI | `client/src/cmbuyer_client/app` | + +## 七、原型 + +- Web:`docs/design/web-task-create.html`、`web-task-workbench.html`、`web-task-detail.html`。 +- Desk:`docs/design/desk-execution.html`;配置结构见 `desk-device-settings.html`。 +- 原型只使用假数据、无网络和生产副作用;显著流程变更先更新原型再实现。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 0058ad0..c2daa6a 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -17,8 +17,8 @@ | T-0xx | Phase 0 地基 | | T-1xx | Phase 1 真机取证 | | T-2xx | Phase 2 采购服务核心 | -| T-3xx | Phase 3 双端打通与第一趟试选 | -| T-4xx | Phase 4 第二趟下单与收尾 | +| T-3xx | Phase 3 双端打通与单趟围栏前执行 | +| T-4xx | Phase 4 真实提交与收尾 | | T-5xx | V2 及以后(图搜、Excel、ERP、订单核对、AI 辅助) | ## 领取规则 diff --git a/docs/tasks/T-103.md b/docs/tasks/T-103.md index 2398464..48e4fd3 100644 --- a/docs/tasks/T-103.md +++ b/docs/tasks/T-103.md @@ -2,7 +2,7 @@ id: T-103 title: 验证规格面板精确选择与 SKU 单价 phase: 1 -deps: [T-102, T-110] +deps: [T-102, T-110, T-111] status: DOING created: 2026-08-04 vikunja_task_id: 23 @@ -25,39 +25,40 @@ write_paths: - docs/current-state.md --- - + ## 问题 / 背景 -T-102 已证明 canonical 链接可进入目标商品。T-103 在 PKG110 / Android 16 / 拼多多 8.17.0、goods_id `937122477375` 上确认:规格面板只能由详情页精确唯一的“快要抢光”打开;T-110 已把该入口批准为可逆、能力受限的第一趟导航。面板刚打开时目标颜色“黑色CHA(纯棉)”和尺码“M(建议100-115)”自动选中。 +T-102 已证明 canonical 链接可进入目标商品。T-103 在 PKG110 / Android 16 / 拼多多 8.17.0、goods_id `937122477375` 上确认:规格面板由详情页精确唯一的“快要抢光”打开;T-110 已把该证据/版本绑定入口批准为受控导航。面板刚打开时目标颜色“黑色CHA(纯棉)”和尺码“M(建议100-115)”自动选中。 -真实 `t103-privacy-v5` 派生曾成功用于安全分析:顶部价格节点为“快卖完 ¥12.88”,并列原价为“¥29.88”;派生截图的整宽遮罩把顶部价格文字遮住一半。项目所有者确认 cmbuyer 是内部系统,要求停止遮罩返工:正式流程可以把包含页面地址/手机号的原始截图上传采购服务供已登录管理员查看。完整 XML 不上传,地址/手机号不解析为业务字段,支付凭据仍禁止处理。 +项目所有者已把 MVP 改为单趟:管理员点击“开始采购”即授权采购工具按锁定的商品、规格、数量和最高总价创建待付款订单,不再有试选后确认。T-103 仍只验证规格选择与读价,是生产单趟的窄前置;数量、确认页、提交围栏和提交订单由后续取证任务开放。系统绝不自动付款。 + +`t103-privacy-v5` 派生 XML 已由人确认保留目标规格、顶部当前价“快卖完 ¥12.88”和原价“¥29.88”,且不含实际地址/手机号;底部“提交订单 ¥12.88”是硬拒绝区。项目已停止遮罩器开发。正式流程允许把规格/确认页内部原始截图上传采购服务,完整 XML 只留客户端本机。 ## 关联需求与交互 -- 功能:F-006 第一趟试选;覆盖 `open_trial_sku_panel()`、`select_sku_options()`、`read_sku_unit_price()` 和安全退出的真机 spike。 -- 用户故事:US-003、US-004、US-008;本任务无生产 GUI。T-103 先验证本机真机能力,T-204 接入原始截图 HTTP 上传与管理员查看。 -- 架构 / API:`docs/04-architecture.md` 第三、四、5.4、六节;`docs/api.md` 的 `TrialSkuFlow`;T-104、T-105 与 Phase 2 的前置。 +- 功能:F-006 单趟采购的规格面板前置;覆盖 `open_sku_panel()`、`select_sku_options()`、`read_sku_unit_price()` 和安全退出的真机 spike。 +- 架构/API:`docs/04-architecture.md` 第三、四、5.4、六节;`docs/api.md` 的 `SkuSelectionFlow`;T-104、T-105 与 T-205 的前置。 +- 本任务没有生产 GUI,也不调用 T-204 的 HTTP 截图链路。 ## 方案 -1. 停止修改或调用截图遮罩器。T-103 的截图先保存在 `%LOCALAPPDATA%\cmbuyer\artifacts\T-103\...` 完成本机验收;T-204 正式接入原始截图上传,资产标记 `privacy_tier=INTERNAL_RAW`,允许包含页面已显示的地址和手机号。 -2. 完整 XML 只留采购工具本机,不上传、不进日志、Vikunja 或 Git。以已确认的 v5 派生 XML 提取只含页面判据节点的最小 fixture;fixture 不含地址、手机号或支付凭据。 -3. 实现 `TrialSkuFlow` 窄 capability。`open_trial_sku_panel()` 只允许拼多多 8.17.0、goods_id `937122477375` 上证据绑定、精确唯一的 `快要抢光`;缺失、重复、版本失配或面板判据不唯一时零后续点击。其他购买文案不得复用。 -4. 第一趟只含打开商品、受控打开面板、在维度容器内精确选择、读回选中态、读取 SKU 单价、原始截图和退出。不得暴露通用 `click`、数量、确认页、提交订单或支付能力;“提交订单”、微信支付、先用后付和 0 元下单只作为硬拒绝判据。 -5. 规格选项按维度容器隔离后文本精确唯一匹配,防 `红`/`粉红`、`1`/`10` 前缀碰撞;缺失、重复、禁用、维度不明或选中态不唯一均停止。 -6. 单价只从已取证规格面板结构中的当前价角色读取,严格解析人民币两位小数字符串。促销前缀会变化,不能把“快卖完”作为主判据;必须限定 PDD package、面板结构/相对位置、非点击价格叶节点、唯一当前价,并排除原价及“提交订单/支付/优惠/预估”等语义。任一歧义返回 unreadable,不从详情页或底部提交区补值。 -7. 真机运行时在内存中读取当前页面树,只返回规格、选中态、价格和页面状态摘要;地址/手机号不解析为结构化字段。原始截图可由 T-204 显式上传,但截图上传器不得枚举目录或顺带上传 XML/manifest。 -8. 离线主审通过后,由人先把颜色和尺码改成明确记录的非目标值,再运行脚本恢复目标规格、读取 `12.88`、保存原始截图并安全退出。`needs_device=true`,agent 保持 DOING,等待人工验收。 +1. 停止修改、调用或扩展截图遮罩器。T-103 原始截图保存在本机产物目录完成真机验收;T-204 以后显式上传 `privacy_tier=INTERNAL_RAW` 的截图。完整 XML 不上传、不进日志、Vikunja 或 Git。 +2. 从已确认 v5 派生 XML 提取只含页面判据节点的最小 fixture;fixture 不含地址、手机号或支付凭据。 +3. 实现 `SkuSelectionFlow` 窄 capability。`open_sku_panel()` 只允许拼多多 8.17.0、goods_id `937122477375` 上证据绑定、精确唯一的“快要抢光”;缺失、重复、版本失配或面板不唯一时零后续点击。其他购买文案不得复用。 +4. 本任务只含打开商品、受控打开面板、维度内精确选择、选中态读回、读取 SKU 单价、原始截图和安全退出。不得暴露通用 `click`、数量、确认页、授权、提交围栏、提交订单或支付能力。 +5. 规格选项按维度容器隔离后精确唯一匹配,防“红/粉红”“1/10”前缀碰撞;缺失、重复、禁用、维度不明或选中态不唯一均停止。 +6. 单价只从已取证规格面板当前价角色读取,严格解析人民币十进制字符串。促销前缀可变,不能把“快卖完”作为主判据;必须限定 PDD package、面板结构/相对位置、非点击价格叶节点、唯一当前价并排除原价、提交/支付/优惠语义。歧义返回 unreadable,不从详情页或底部按钮补值。 +7. 真机运行只返回规格、选中态、价格和页面状态摘要;地址/手机号不解析为字段。截图上传器不属于本任务。 +8. 离线主审通过后,由人先把颜色和尺码改成记录的非目标值,再运行脚本恢复目标规格、读取 12.88、保存原始截图并安全退出。`needs_device=true`,agent 保持 DOING 等人工验收。 ## 验收要点 -- 最小 fixture 只来自本项目拼多多 8.17.0、goods_id `937122477375` 的已验收 v5 派生 XML,不含地址、手机号或支付凭据。 -- `快要抢光` 精确唯一时才打开面板;其他入口、版本失配、面板不唯一、规格碰撞/禁用、选中态或价格不唯一均 fail closed。 -- 价格读取不依赖可变促销文案,能区分当前价 `12.88`、原价 `29.88` 和硬拒绝区“提交订单 ¥12.88”;金额只用十进制字符串。 -- 静态 API/导入测试证明第一趟无通用点击、数量、确认页、提交订单或支付能力。 -- T-103 不再开发遮罩;T-204 原始截图上传必须限制为显式截图文件、设备鉴权、SHA-256 校验和管理员会话读取,不上传完整 XML 或支付凭据。 -- client 全部单测、compileall、wheel metadata、上下文校验、完整 `init.ps1` 与 diff-check 通过。 -- 只有人完成受控入口、精确规格选择、单价读取、页面对应性、原始截图与安全退出验收后才能 DONE。 +- 最小 fixture 只来自本项目拼多多 8.17.0、goods_id `937122477375` 已验收的 v5 派生 XML,不含地址、手机号或支付凭据。 +- 受控入口、规格容器、精确选择、选中态和当前价任一歧义均 fail closed;价格能区分 12.88、原价 29.88 与硬拒绝区“提交订单 ¥12.88”。 +- 静态 API/导入测试证明 `SkuSelectionFlow` 无通用点击、数量、确认页、授权、围栏、提交或支付能力。 +- T-103 不再开发遮罩。T-204 的原始截图上传必须限制为显式截图文件、设备鉴权、SHA-256 和管理员会话访问,不上传完整 XML 或支付凭据。 +- client 全部单测、compileall、wheel metadata、上下文校验、完整 init 与 diff-check 通过。 +- 只有人完成受控入口、精确规格选择、SKU 单价语义、页面对应性、原始截图与安全退出验收后才能 DONE。 ## 执行记录 @@ -164,6 +165,14 @@ T-103 sanitizer v2 坐标修正与主审:提交 44c027a 将 screenshot space ### 2026-08-04T07:45:57Z · ila 2026-08-04 最终范围澄清:上一条“正式截图仍需脱敏/裁剪”的记录已被项目所有者新决定取代。cmbuyer 是内部系统,正式第一趟允许把包含页面已显示地址/手机号的原始截图上传采购服务,不做遮罩或裁剪;T-204 负责设备鉴权上传、SHA-256 校验与管理员会话访问。完整 XML、普通日志、Git、Vikunja 和支付凭据仍不接收。T-103 立即停止遮罩开发,继续受控规格选择与 SKU 读价,任务保持 Doing。 + +### 2026-08-04T07:52:38Z · ila + +2026-08-04:项目所有者确认管理员点击“开始采购”即授权创建待付款订单,MVP 从两趟人工确认改为单趟采购。T-103 暂停,等待 T-111 先重写状态机、授权、价格闸门与任务依赖;暂停期间不得继续实现 TrialSkuFlow 或任何数量/确认页/提交代码。T-103 仍为未完成任务。 + +### 2026-08-04T08:24:12Z · ila + +2026-08-04:T-111 单趟契约主审通过,T-103 解除架构阻塞。恢复后只实现 SkuSelectionFlow:受控入口、维度内精确选择、选中态读回、SKU 当前价唯一读取、原始截图和安全退出;不得包含数量、确认页、授权、提交围栏、提交订单或支付。客户端工作树现有未提交 v6 遮罩测试残留必须先撤销到已提交 v5 基线,再开始新实现。needs_device=true,离线实现后仍等待人工真机验收。 ## 边界 @@ -171,7 +180,7 @@ T-103 sanitizer v2 坐标修正与主审:提交 44c027a 将 screenshot space - 任何规格面板入口、维度容器、选项、选中态和价格判据都必须来自本项目拼多多 8.17.0 真机证据; T-102 详情页证据只能证明已到目标商品,不能证明规格面板结构。T-103 只使用已由人确认无实际地址/ 手机号的 v5 派生 XML 提取最小 fixture;派生截图的上传适用性不再是本任务的代码门禁。 -- 第一趟只允许 T-110 批准的 `open_trial_sku_panel()` 窄能力点击证据/版本绑定、精确唯一的 +- `SkuSelectionFlow` 只允许 T-110 批准的 `open_sku_panel()` 窄能力点击证据/版本绑定、精确唯一的 `快要抢光`;当前事实仅覆盖 goods_id `937122477375`、拼多多 `8.17.0`。不得把“免拼购买 / 单独购买 / 直接拼成”等其他文案加入包含、前缀、同义或坐标兜底,分别取证前一律拒绝。 - 规格选项只能在已确认的面板及对应维度容器内按文本精确唯一匹配;缺失、重复、禁用、维度不明、 @@ -180,7 +189,8 @@ T-103 sanitizer v2 坐标修正与主审:提交 44c027a 将 screenshot space - 单价只允许从规格面板证据确认的节点读取,并以十进制字符串表达;不得使用浮点,不得从详情页、 搜索卡片或其他页面的数字补值。券后价、原价、区间价、货币符号拆分或多个候选的语义无法唯一 证明时必须返回 unreadable 并转人工。 -- T-103 代码路径不得引用或实现 `set_quantity()`、`go_to_order_confirm()`、`submit_order()`、 +- T-103 代码路径不得引用或实现 `set_quantity_and_readback()`、`go_to_order_confirm()`、 + `create_submission_fence()`、`submit_order_once()`、 通用 `click`、创建订单、授权、提交围栏或任何支付能力;不得进入订单确认页,不得创建待付款订单。 规格面板中的“提交订单”、微信支付、先用后付、0 元下单只可作为硬拒绝判据,不能返回可点击对象。 - 不把 `/mnt/d/chengma/cmroubao`、`/mnt/d/chengma/cmpdd` 或任何旧版本的节点、Activity、选择器、 @@ -188,7 +198,7 @@ T-103 sanitizer v2 坐标修正与主审:提交 44c027a 将 screenshot space - 原始完整 XML 只保存在 `%LOCALAPPDATA%\cmbuyer\artifacts\T-103\...\raw`,不得由 HTTP sink、 Vikunja 或 Git 读取/上传。真机流程可在内存中读取当前页面树,但只返回规格、选中态、价格和页面 状态摘要;不得把地址、手机号或支付凭据解析成业务字段或写入日志。 -- T-103 不再扩展截图遮罩器;本任务先以本机原始截图完成真机验收。生产第一趟原始截图上传由 T-204 +- T-103 不再扩展截图遮罩器;本任务先以本机原始截图完成真机验收。生产单趟原始截图上传由 T-204 接入,允许截图包含页面已显示的地址和手机号,但只允许已认证设备上传、已登录管理员查看;完整 XML、 外部支付页和支付凭据不得上传。 - `needs_device: true`:agent 只能完成离线实现并保持 `DOING`;只有人完成安全入口、精确规格选择、 diff --git a/docs/tasks/T-111.md b/docs/tasks/T-111.md new file mode 100644 index 0000000..6205d40 --- /dev/null +++ b/docs/tasks/T-111.md @@ -0,0 +1,85 @@ +--- +id: T-111 +title: 调整为开始采购授权的单趟流程 +phase: 1 +deps: [T-004, T-010, T-110] +status: DONE +created: 2026-08-04 +vikunja_task_id: 28 +context_ref: efeb2d9 +work_branch: task/t-111-single-pass +needs_device: false +needs_human_review: false +write_paths: + - AGENTS.md + - README.md + - docs/00-ai-start-here.md + - docs/01-vision.md + - docs/02-requirements.md + - docs/03-tech-stack.md + - docs/04-architecture.md + - docs/05-coding-rules.md + - docs/06-tasks.md + - docs/07-user-stories.md + - docs/08-interaction-checklist.md + - docs/api.md + - docs/current-state.md + - docs/README.md + - docs/routes.md + - docs/design/README.md + - docs/design/desk-execution.html + - docs/design/web-task-create.html + - docs/design/web-task-detail.html + - docs/design/web-task-workbench.html + - docs/tasks/README.md + - docs/tasks/T-103.md + - docs/tasks/T-111.md +--- + + +## 问题 / 背景 + +项目原设计为两趟:先试选并等待管理员确认,再重新进入商品创建待付款订单。项目所有者为加快内部采购,决定取消试选后的人工确认;管理员在任务列表勾选并点击“开始采购”即明确授权采购工具创建一笔待付款订单。系统仍绝不自动付款。 + +## 关联需求与交互 + +- 重写 MVP 主流程、状态机、价格闸门、授权与提交围栏。 +- 同步需求、用户故事、交互、路由、API、任务路线图、原型与当前状态。 +- T-103 改为单趟流程的规格选择/读价前置;T-202 只建 DRAFT,不受影响;新增 T-209 先迁移旧 schema/状态机,T-203 起按新流程实现。 + +## 方案 + +1. DRAFT 任务只有管理员勾选并点击“开始采购(只创建待付款订单)”后才能进入设备队列;该动作在服务端事务中创建一次性授权,锁定任务版本、goods_id、颜色、尺码、数量和最高总价。 +2. 采购工具领取后同一趟完成:打开商品、受控打开规格面板、精确选规格、闸门一读价并校验上限、设置及复核数量、闸门二重读同价与选中态、进入确认页、闸门三核对应付总额、服务端原子提交围栏、只点击一次“提交订单”。 +3. 删除 WAITING_CONFIRMATION、人工“机器选对了吗”、授权锁定试选价和第二次重新打开商品;截图改为内部审计证据,不作为下单前人工审批。 +4. 授权在围栏前失败/过期可回到 DRAFT 等管理员重新开始;围栏建立后只能调和同一提交,禁止重试、释放或签发第二笔。 +5. 不付款、提交四条件、精确规格、数量复核、价格只读规格面板/订单确认页、外部支付/安全校验停止等红线不放宽。 +6. 先提交文档契约,再恢复客户端或新增生产代码。 + +## 验收要点 + +- 所有活跃文档只描述单趟采购,不再把两趟或 WAITING_CONFIRMATION 当作 MVP 现实。 +- “开始采购”与一次性授权、批量原子性、价格闸门、提交围栏和一次点击的关系明确。 +- API/状态机/目标数据模型/任务路线图一致;当前旧 schema 的迁移由 T-209 显式前置。 +- 截图为内部证据而非审批前置;T-103 新边界继续禁止数量、确认页和提交。 +- Vikunja 导出、agent-context、HTML/JS 静态检查、diff-check 与完整双端门禁通过。 + +## 执行记录 + +### 2026-08-04T08:24:12Z · ila + +2026-08-04 T-111 主审完成:已把 AGENTS、入口、愿景、需求、架构、编码规则、API、路线图、用户故事、交互、路由、当前状态及四个两端 HTML 原型统一为“管理员点击开始采购即授权创建待付款订单”的单趟流程。保留三道价格闸门、提交四条件、服务端原子围栏、首次许可后只点一次和绝不付款;删除 WAITING_CONFIRMATION / PENDING_RETRIAL / AUTHORIZED 等旧 MVP 状态。审计发现生产 v1 schema 仍为旧 spec_trials/锁定试选价模型,因此路线图新增 T-209,先迁移 schema/领域状态机,再允许 T-203 实现新授权事务。 + +独立验证通过:四个 HTML 由 Python HTMLParser 解析;四个 JavaScript 由 Node new Function 解析;桌面 18 个 scenario/options/sessionControls 一致;web 关键旧状态不存在;Vikunja T-103/T-111 export check;validate_agent_context;git diff --check;完整 init.ps1(admin test/vet/build,client 80 tests/compileall,包安装与上下文门禁)。未连接真机、未触发采购/提交/付款;未触碰用户 docs/ppt。 + + +## 边界 + +- 管理员点击“开始采购(只创建待付款订单)”是进入设备队列前唯一的人类授权动作;创建 `DRAFT` + 本身不得触发设备领取、创建授权、提交订单或付款。 +- 单趟流程仍必须保留规格精确匹配、数量读回复核、规格面板两次同价校验、订单确认页金额上限、 + 服务端提交围栏、提交控件唯一和点击一次;不得以“提速”为由删除或弱化任一项。 +- 系统只创建待付款订单,任何路径都不自动付款,不点击支付、免密支付、先用后付或扣款控件。 +- 围栏前失败或授权过期可以回到 `DRAFT` 等管理员重新授权;围栏建立后只能调和同一提交记录, + 不得释放、重领、重试点击或签发第二笔。 +- 本任务只修改文档契约和任务依赖,不修改 `admin/`、`client/`、数据库 migration 或生产代码。