Files
cmbuyer/docs/04-architecture.md
T

32 KiB
Raw Blame History

架构设计

本文定义系统结构、职责、单趟采购流程、安全边界和数据模型。框架与运行命令见 技术栈,双端线协议以 API 合约为唯一权威。

一、系统结构

人工填链接(MVP)                 Excel / ERP(V2)
         │                              │
         └──────────────┬───────────────┘
                        v
        ┌─────────────────────────────────────────┐
        │  采购服务(admin/,Go)                   │
        │  · 建单、查询、开始采购授权与任务状态        │
        │  · 提交围栏、结果调和、内部证据与审计         │
        │  · 服务端渲染管理页面                       │
        └───────────────────┬─────────────────────┘
                            │ 本机回环 HTTP / JSON(MVP)
                            │ Bearer + 设备绑定;非回环前必须先上 TLS
                            v
        ┌─────────────────────────────────────────┐
        │  采购工具(client/,Python + PySide6)    │
        │  · 轮询领取、单趟执行、回传状态与证据        │
        │  · 本地完整节点树与执行轨迹                  │
        └───────────────────┬─────────────────────┘
                            │ ADB(USB / WiFi)
                            v
                 Android 手机(拼多多 App)
  • 采购服务:Go + gin,SQLite,goose migration,本地 SHA-256 证据存储。
  • 采购工具:Python + uiautomator2 + PySide6;执行器只依赖 TaskSource / ResultSink 抽象。
  • 页面判据与拼多多 App 版本绑定;版本不同即停止,不把前序项目页面结构当作事实。

二、职责划分

采购服务(admin/)

独占以下权威:

  • 任务创建、批量开始采购、状态流转和终态判定;
  • 一次性采购授权的签发:管理员点击“开始采购”是唯一的人类授权动作;
  • 任务不可变字段和最高总价校验;
  • 提交订单前的原子围栏与点击后结果调和;
  • 管理员会话、设备凭据、内部截图证据和审计记录。

采购服务不连接手机、不发 ADB 命令、不解析拼多多页面,也不持有支付或 AI provider 凭据。

采购工具(client/)

独占以下设备能力:

  • ADB 连接、设备健康检查和已取证 App 版本校验;
  • 打开商品、识别页面、精确选规格、设置数量、读取价格;
  • 在满足全部门禁并取得服务端围栏后,精确点击一次“提交订单”;
  • 截图、完整节点树和执行日志的本地采集,显式上传内部截图。

采购工具不自行修改任务约束、不扩大金额上限、不领取 DRAFT,也不能签发授权。没有服务端 明确返回 click_permitted=true 时,任何本地判断都不能创建订单。

权威冲突规则

服务端校验锁定的任务约束,客户端校验当前真机事实。任一端拒绝或两端摘要不一致,一律停止并 转人工;不能为了“继续跑”选择相信其中一端。

三、单趟采购执行

MVP 只做任务自带商品链接的 A 路径。创建任务与开始采购分离,但管理员开始后不再插入试选确认:

管理员创建 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、1080×2376 的规格面板 入口是屏幕底部购买区第一行 快要抢光 + 金额,不是商品内容区的同名促销小字。T-110 已批准把 该特定证据、版本和页面状态绑定的点击定义为受控导航。

  • 只能精确唯一匹配底部非点击文本叶节点、其直接可点击 PDD 父容器及证据绑定结构,并且只点击 第一行文本叶节点中心;缺失、重复、版本失配、结构漂移或打开后面板不唯一时零后续点击。
  • 商品内容区同名促销小字明确禁止作为入口。底部按钮中的金额只参与结构识别,不得解析、返回或 充当价格来源;价格仍只能从规格面板和订单确认页读取。
  • 免拼购买 只允许作为已取证、不可点击的精确兄弟节点验证底部结构,不能作为选择器、目标或兜底; 它变为可点击、位置漂移或出现在其他结构时必须拒绝。“单独购买 / 直接拼成”等其他文案同样不能 用包含、前缀、同义或坐标兜底。
  • 受控入口只负责进入已取证面板,不等于支付授权,也不能暴露通用任意点击能力。
  • T-103 的隔离验证 capability 只包含开商品、开面板、选规格、读价和安全退出;数量、确认页、 提交和支付仍由后续真机任务分别取证后才能接入生产单趟执行器。

2026-08-05 的五组 8.17.0 真机证据进一步证明,面板刚打开时颜色与尺码均未选中,尺码选项尚未 进入视口;选择目标颜色后,摘要只剩“请选择:尺码”,但尺码仍不可见。实现必须把未选初态、 仅颜色已选态、非目标尺码态和目标尺码态作为各自完整的证据 profile 匹配,禁止把不同状态的价格、 摘要、容器或坐标拼成一个宽松判据。

  • 显示尺码只允许命名的私有 reveal_size_options() 窄能力:仅从已证明的“目标颜色已选、尺码未选 且隐藏”状态执行,绑定证据确认的嵌套滚动容器与列间安全通道,最多一次且不重试;不得公开通用 swipe、scroll、任意坐标或把底部提交区纳入动作范围。
  • 生产 reveal 落地前,必须补齐“动作前后目标尺码均未选、尺码由隐藏变为可见、提交区未触发”的 同版本真机动作证据。现有“滚动后 M 已选中”快照不能证明滚动没有误点规格,不得据此猜测手势。
  • 这组证据只能由独立 spike 在严格前置成立后执行一次固定、无参数手势采集;生产 Flow 不得调用该 spike。RPC 超时或响应不明时只允许只读调和,不得重试;候选后置必须再由人核对后才能成为生产判据。
  • 尺码 action 的真实节点类型、精确文案和 selected 读回必须随 profile 一起验证;当前证据中的 S/M 是可点击 TextView。只有目标颜色与目标尺码双重读回一致后才能读取闸门一价格。
  • 一次 Back 在调用前即封存且不得重试;仅“面板消失”不能作为生产安全退出的充分条件。同商品详情页 的严格退出判据由 T-104 取得独立真机证据后开放。

三道价格闸门

闸门 当前页面 判据 拒绝条件
一 规格面板,选中目标规格后 单价唯一可读;单价 × 授权数量 <= 最高总价 不可读、有歧义或超上限
二 规格面板,数量读回后 颜色、尺码仍正确;目标数量面板总额唯一可读且不超最高总价 规格/数量漂移、总额不可读/有歧义或超上限
三 订单确认页 规格、数量正确;应付总额唯一可读、严格等于闸门二且不超最高总价 任一不一致、不可读、有歧义或超上限

金额一律使用十进制字符串和十进制定点运算,不用浮点数。价格只从规格面板和订单确认页读取; 详情正文、搜索卡片、底部购买/提交按钮的数字不作为价格来源。

2026-08-06 的拼多多 8.17.0 真机证据证明同一规格的多件优惠是非线性的:数量 1 时规格面板顶部 当前金额为 12.88,数量 2 时变为 32.76,而不是 12.88 × 2。因此闸门一仍是设置数量前的单价 预检,闸门二必须读取设置后的面板总额 gate2_panel_total_price 并直接比较最高总价;不得把 Gate1 乘数量、不得要求 Gate2 等于 Gate1,也不得把底部“提交订单 ¥...”金额作为价格候选或兜底。 Gate3 再要求确认页应付总额与 Gate2 面板总额严格相等,这比只检查上限多一道跨页防漂移约束。

闸门一与闸门二发生在同一设备会话中,不需要管理员在中间确认。旧截图、缓存值和发布前 dry-run 都不能替代这次实时读取。

B 路径(V2)

没有商品链接时由图片搜索只产出 goods_id,再汇入上述流程。不在搜索结果页读取价格或规格。

四、安全边界(硬约束)

以下每条都必须有测试证明,不得在任务中顺手放宽:

边界 规则 防止什么
不付款 不点击支付、免密支付、先用后付或任何扣款控件 真实资金损失
提交四条件 授权+围栏、闸门二、闸门三、控件唯一同时成立,只点一次 误下单 / 重复下单
确认页零点击 除返回和满足四条件后的“提交订单”外不点击任何控件 未知副作用
规格精确匹配 维度内等值唯一匹配,防 红/粉红、1/10 前缀碰撞 买错规格
数量读回复核 设置后精确读回,不一致即停 买错数量
三道价格闸门 任一道不可读、有歧义或不通过都停,不用别处数字凑 超预算
受控页面能力 页面动作按任务与证据分层;不得把通用 click 传入业务流程 边界扩散
外部支付页 检测到外部支付交接立即停止,不读取、保存或输入凭据 凭据泄露
安全校验 验证码、风控、人脸、短信出现即停止,不绕过 封号 / 违规
内部截图 可上传页面已显示的地址/手机号;不解析成字段或日志,完整 XML 不上传 非必要扩散
身份隔离 管理 session+CSRF 与设备 Bearer 分属不同路由域,混合凭据不叠加权限 设备越权 / 会话冒充
Bearer 传输 MVP 仅绑定 IPv4 回环 127.0.0.1:8080;非回环访问先建立 HTTPS/TLS 终止 明文局域网泄露 token
授权一次性 一条任务版本只有一份有效授权;幂等重放不生成第二份 重复采购
服务端提交围栏 点击前原子创建唯一提交记录;失败或响应不明不得点击 并发 / 断网重复下单
App 版本绑定 运行版本不同于证据版本时停止并重新取证 旧判据误点

提交订单的四个前置条件

“提交订单”是系统唯一会创建真实待付款订单的动作。以下四项同时满足才允许点击一次:

  1. 一次性授权有效且服务端提交围栏已建立:围栏把授权、任务、领取和本次验证摘要原子绑定到 唯一 order_submission;明确响应包含 click_permitted=true。
  2. 闸门二通过:目标规格和数量未漂移,目标数量面板总额唯一可读且不超过授权最高总价。
  3. 闸门三通过:确认页规格、数量正确,应付总额严格等于闸门二面板总额,且不超过授权最高总价。
  4. 提交控件唯一:文本精确等于“提交订单”,可点击祖先唯一。

围栏请求超时、断网、冲突或响应不明时不得点击。围栏建立后:

观察结果 处置
明确进入订单结果 / 待付款页 上报 SUBMITTED,任务转 WAITING_PAYMENT
跳转外部支付 立即停止,上报结果不明确,不执行支付
出现验证码 / 风控 / 人脸 / 短信 立即停止,上报结果不明确,不绕过
超时、断连、页面无法判定 转 RECONCILIATION_REQUIRED,保留围栏和金额额度

后三种情况都禁止释放围栏、重新授权、重新领取或再次点击。只能调和同一提交记录。

发布前 dry-run 与生产围栏的区别

首次启用某一 App 版本的真实提交能力前,必须用独立真机任务完成只读 dry-run:进入确认页,验证 规格、数量、金额与提交控件唯一,然后退出且不点击。它用于证明判据和不可达测试,不是每笔采购的 “第一趟”,也不产生可复用页面事实。生产任务仍在同一趟内重新通过三道闸门并申请服务端围栏。

五、数据模型

5.1 核心实体

CREATE TABLE tasks (
  id                 TEXT PRIMARY KEY,
  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,
  updated_at         TEXT NOT NULL
);

-- 管理员点击“开始采购”产生;锁定任务约束,不锁定旧观察价
CREATE TABLE order_authorizations (
  id                 TEXT PRIMARY KEY,
  task_id            TEXT NOT NULL REFERENCES tasks(id),
  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,
  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, 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_panel_total_price TEXT,
  quantity_read      INTEGER,
  confirm_amount     TEXT,
  failure_code       TEXT,
  started_at         TEXT NOT NULL,
  finished_at        TEXT,
  UNIQUE (task_id, claim_generation),
  UNIQUE (task_id, authorization_id, id, claim_generation)
);

-- attempt 的设备/session 所有权与可恢复租约;token 明文永不入库
CREATE TABLE purchase_attempt_claims (
  attempt_id              TEXT PRIMARY KEY,
  task_id                 TEXT NOT NULL,
  authorization_id        TEXT NOT NULL UNIQUE,
  claimed_by_device_id    TEXT NOT NULL REFERENCES device_credentials(device_id),
  session_id              TEXT NOT NULL,
  claim_generation        INTEGER NOT NULL,
  task_version            INTEGER NOT NULL,
  task_title              TEXT NOT NULL,
  authorization_task_version INTEGER NOT NULL,
  goods_id                TEXT NOT NULL,
  sku_color               TEXT NOT NULL,
  sku_size                TEXT NOT NULL,
  quantity                INTEGER NOT NULL,
  total_price_cap         TEXT NOT NULL,
  authorization_expires_at TEXT NOT NULL,
  claim_nonce             BLOB NOT NULL,        -- 32 字节随机 nonce
  claim_token_sha256      BLOB NOT NULL,        -- 32 字节 hash,不是 token 明文
  lease_expires_at        TEXT NOT NULL,
  claimed_at              TEXT NOT NULL,
  closed_at               TEXT,
  FOREIGN KEY (task_id, authorization_id, attempt_id, claim_generation)
    REFERENCES purchase_attempts(task_id, authorization_id, id, claim_generation)
);

CREATE UNIQUE INDEX purchase_attempt_claims_one_open_per_device_idx
ON purchase_attempt_claims (claimed_by_device_id) WHERE closed_at IS NULL;

-- claim-next 的 CLAIMED / EMPTY / BLOCKED 与 renew CAS 都持久化,保证跨重启幂等。
-- 复合外键同时绑定 attempt、设备、session、generation 与 token hash,应用 bug 不能跨归属写事实。

-- 真机真实点击前建立;一份授权最多一条
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),
  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_panel_total_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 (attempt_id)
);

-- 设备 token 明文只在签发完成后显示一次;数据库仅保存原始 32 字节 token 的 SHA-256
CREATE TABLE device_credentials (
  device_id     TEXT PRIMARY KEY,              -- 规范小写 UUIDv4
  display_name  TEXT NOT NULL,                 -- 非秘密运维名称
  token_sha256  BLOB NOT NULL UNIQUE,           -- 恰好 32 字节
  status        TEXT NOT NULL,                  -- ACTIVE | REVOKED
  created_at    TEXT NOT NULL,
  revoked_at    TEXT,
  CHECK ((status = 'ACTIVE' AND revoked_at IS NULL)
      OR (status = 'REVOKED' AND revoked_at IS NOT NULL AND revoked_at >= created_at))
);

-- INTERNAL_RAW 原始截图;原文件名和客户端路径不进入数据库
CREATE TABLE evidence_assets (
  id                    TEXT PRIMARY KEY,
  upload_key            TEXT NOT NULL,
  task_id               TEXT NOT NULL,
  attempt_id            TEXT NOT NULL,
  kind                  TEXT NOT NULL,          -- T-204 仅 SKU_PANEL_GATE_1
  privacy_tier          TEXT NOT NULL,          -- 仅 INTERNAL_RAW
  sha256                TEXT NOT NULL,          -- 64 位小写十六进制
  byte_size             INTEGER NOT NULL,
  content_type          TEXT NOT NULL,          -- 仅 image/png
  width_px              INTEGER NOT NULL,
  height_px             INTEGER NOT NULL,
  storage_key           TEXT NOT NULL,          -- 由 SHA-256 唯一派生
  uploaded_by_device_id TEXT NOT NULL,
  captured_at           TEXT NOT NULL,
  created_at            TEXT NOT NULL,
  UNIQUE (uploaded_by_device_id, upload_key),
  FOREIGN KEY (task_id, attempt_id) REFERENCES purchase_attempts(task_id, id)
);

MVP 不再用 spec_trials 作为审批记录,也不存在 authorized_unit_price。实际读价属于 purchase_attempts / order_submissions 的执行与审计事实;管理员授权的资金边界始终是 total_price_cap。

设备凭据由本机管理 CLI 签发、列出和撤销。token 是 32 字节加密随机值,以 64 位小写十六进制 只显示一次;服务端把 token 解码回原始字节后计算 SHA-256,并与按设备 id 查出的 32 字节 BLOB 恒定时间比较。未知 id 也执行固定宽度 dummy compare。认证逐请求查库,因此撤销事务提交后才开始的 请求全部拒绝;提交前已经完成认证的在途请求不追溯取消。格式/未知/错配/撤销统一空 401,存储故障 空 503,两类都在读取业务请求体前失败闭合。

5.2 状态机

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 已保存,未授权;管理员可编辑/取消或点击开始采购;设备不可领取
PENDING 已有有效一次性授权,等待采购工具领取
CLAIMED 已由一个设备实例持有租约,尚未开始页面操作
ORDERING 单趟执行中,正在选规格、过闸门或申请围栏
NEEDS_MANUAL 围栏前失败;显示原因,由人核查后重置为 DRAFT 或取消
WAITING_PAYMENT 订单已明确创建,等待人在拼多多付款;不是成功
RECONCILIATION_REQUIRED 围栏后结果不明;只能核查同一提交,不能重试
SUCCEEDED 人已付款并完成核对
FAILED 人工调和确认订单未创建或任务无法完成
CANCELED 围栏前由管理员取消,不再执行

批量 DRAFT → PENDING 必须全有或全无。服务端同时创建授权;“先改状态、稍后补授权”无效。 WAITING_CONFIRMATION、PENDING_RETRIAL、AUTHORIZED 和 RUNNING(TRIAL) 不再属于 MVP 状态。

5.3 授权、租约与恢复

  • claim/renew 使用独立 32 字节 HMAC secret;配置为 64 位小写十六进制,不得等于 session secret 或 任一设备 token。每条 claim 以版本域分隔 HMAC 绑定 device/task/authorization/attempt/generation/ 32 字节随机 nonce,SQLite 只保存 nonce 和 token SHA-256。启动会重建并恒定时核对所有 open/closed claim;secret 错误时失败闭合,不轮换 token。
  • claim 与 renew 的 SQLite 事务必须先通过条件 no-op UPDATE 取得写入线性化位置并复核设备 ACTIVE, 才能读取或重放 request。撤销先提交则请求失败;claim/renew 先取得写位置则该事务可完成,随后撤销 不会自动释放已建立的 claim。
  • 同一授权最多一个 attempt,同一设备最多一个未关闭 claim。EMPTY、人工恢复冲突和续租响应都持久化; 同幂等键只能重放原结果,不能因候选变化、续租或服务重启递增 generation、轮换 token 或延长第二次。
  • claim 行同时冻结成功响应所需的 task 标题/版本以及 authorization 版本、规格、数量、总价上限和到期 时间。旧 request 跨重启只从该快照重建;源 task/authorization 后续漂移不能改变历史响应,并会让新 恢复或续租失败闭合。进入 ORDERING 的同 attempt 仅接受 task version 恰好比 claim 快照加一。
  • 租约 TTL 显式配置为正且严格短于授权 TTL;新到期时间不得超过授权到期。租约与授权边界相等即过期, 没有宽限;过期、撤销、停止轮询和进程退出均不关闭 claim、不释放授权、不允许另一设备接管。
  • 授权带 expires_at,只有围栏前可转 EXPIRED / ABANDONED;任务回到 DRAFT,必须重新点击 开始采购。旧授权永不复活。
  • 设备租约丢失不等于授权可安全重用。只有服务端确认该 attempt 未建立围栏,才能关闭 attempt 并 回到 DRAFT / NEEDS_MANUAL;不能自动重新领取并重复页面动作。
  • 围栏建立后即使租约过期也只恢复同一 order_submission 的调和,不能回到可领取队列。
  • 每种非终态都必须给出安全下一步,不能出现隐藏表单导致任务永久锁死。

采购工具本地恢复状态

  • 生产入口先取得基于规范数据库路径的 Windows Global\ named mutex,再构造 DPAPI 和 SQLite;不能 以服务端“单设备最多一个 claim”替代本机单实例。
  • profile、polling session、claim/renew request、open/historical claim、evidence marker/slot 使用 WAL、 synchronous=FULL 与 append-only/单调关闭约束。当前 session/claim 由 closed_at IS NULL partial unique 保证唯一,历史关闭后不阻塞下一条,但不得删除或复活。
  • DurableClientGateway 是轮询和截图接入的唯一顺序入口:先 durable prepare,再一次 HTTP,最后原子 commit。停止只把当前 session 的 accept_new 设为 false;飞行中响应仍提交,pending/open 不清除。
  • 每次发送前校验 profile/session/request/active immutable business snapshot、renew history、evidence marker/slot/ receipt 的完整状态图;snapshot、renew response 与 evidence receipt 还保存不可变摘要。任一冗余事实不一致、 DPAPI context 不匹配、数据库缺行或文件 identity 变化时零 HTTP 停止,不能自行“修复”。
  • 设备 Bearer 的 401 发生在服务端读取 body/写幂等事实前,因此槽保持 PENDING;只允许同 device id 更新 Bearer,并在用户再次开始后用原 key/body/file 尝试。claim token 和其他 frozen 配置不变。
  • 原始 token 以 DPAPI current-user context 密文保存:device token 绑定 profile+device,claim/renew token 绑定 profile+attempt,密文不能跨行复用;日志 formatter 对 Bearer、裸 64 位 token 和 traceback 做最终脱敏。异常对象也只保留固定 reason,不挂接含响应 body/partial/path 的异常上下文。

5.4 证据分层

数据 位置 边界
商品 / 规格 / 确认页原始 screenshot 采购工具本机 + 采购服务内部证据存储 可含页面已显示地址/手机号;设备鉴权上传、管理员登录查看,不遮罩
完整 XML 仅采购工具本机隔离目录 可在内存解析页面判据;不上传、不写日志、Git、Vikunja
最小 XML fixture 采购工具测试 / Git 只保留判据所需结构,确认无地址、手机号、支付凭据
外部支付页或支付凭据 不保存、不上传 检测到交接立即停止
AI 调用记录(V2) 仅采购工具本地 不进入采购服务

截图上传器只能接收调用方显式指定的截图,不能枚举证据目录或顺带上传 XML/manifest。证据响应 使用 Cache-Control: no-store,不能暴露为免登录静态目录。

内部截图存储采用以下固定边界:

  • 单个 PNG 最大 10 MiB、单边最大 8192 px、总像素最大 16,777,216;同时验证 multipart MIME、 PNG 魔数、完整解码、字节数、尺寸和调用方声明的 SHA-256。
  • 上传 handler 必须先通过逐请求 SQLite 设备认证并再次校验规范 principal,再解析 Content-Type 或 读取 body。空库、无效或已撤销凭据拒绝,认证存储故障返回 503;不把管理员 session 当设备身份。
  • 首次 evidence INSERT 必须由 store 校验和 SQLite trigger 双重证明 (task, attempt, authenticated device) 对应未关闭 claim。历史同设备/upload key 重放先于该检查,因此人工关闭 claim 不会破坏已提交资产的 幂等读取;关闭后禁止新 upload key,且不为此增加 token/session 字段或放宽截图 kind。
  • 文件写入显式配置的私有证据根目录:同目录随机临时文件 → 流式 hash → 校验 → fsync → 原子 rename 到 SHA-256 内容地址 → 最后事务写数据库。数据库永远不指向半文件或缺失文件。
  • SQLite 与文件系统不能组成跨资源事务;极端故障最多留下不可达孤儿文件。不得为清理孤儿而删除 可能被其他资产记录并发复用的内容文件,自动保留/删除策略留给部署任务。
  • SHA-256 只用于物理内容寻址,不是业务资产唯一键;不同合法证据可以引用相同内容。同设备主体与 upload_key 同载荷重放原资产,任一规范字段变化即冲突。

六、关键技术难点

难点 风险 应对
页面结构随版本变化 旧选择器误点新页面 每条判据先真机取证,记录 App 版本、截图、XML、goods_id
购买语义入口才打开面板 能力范围易扩散 只批准证据绑定的精确唯一入口;每种文案单独取证
当前价与原价/按钮价混杂 读错价格 限定已取证结构和语义;价格只在面板/确认页读,歧义即停
单趟页面状态变化 数量或促销导致金额非线性变化 同一趟读 Gate1 单价与 Gate2 面板总额,再要求确认页总额与 Gate2 严格相等
WiFi ADB / 双通道 断连或操作错设备 serial 必填;USB/WiFi 同设备或身份不明时 fail closed
不可逆动作超时 可能已创建订单 服务端围栏 + 点击一次 + 只调和,不重试
双端契约漂移 静默不兼容 api.md 唯一权威;契约改动跑完整双端门禁

七、推荐开发顺序

  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、多设备。

T-103 继续作为“规格选择与读价”的隔离前置,不含数量、确认页或提交。生产单趟并不意味着在一个 任务里跳过逐段取证;它只意味着这些已验证能力集成后,每笔业务任务不再等待中途人工确认。

八、项目结构

cmbuyer/
├── docs/
├── admin/
│   ├── cmd/server/
│   ├── internal/domain/
│   ├── internal/usecase/
│   ├── internal/transport/httpapi/
│   ├── internal/transport/webui/
│   ├── internal/storage/
│   └── migrations/
├── client/
│   ├── src/cmbuyer_client/device/
│   ├── src/cmbuyer_client/pdd/
│   ├── src/cmbuyer_client/core/
│   ├── src/cmbuyer_client/remote/
│   ├── src/cmbuyer_client/app/
│   └── tests/
└── scripts/

执行器依赖核心端口,不直接读取 Excel 或拼接 HTTP。T-303 只提供 TaskSource / EvidenceSink;完整 ResultSink 在服务端 events/fail/fence/result 契约落地后分阶段组合,来源变化不得改变安全执行器。

九、架构纪律

  • 业务事实和 schema 变化同步本文与 api.md,代码不得另起一套字段或状态。
  • 第四节安全边界只能收紧。需要变更时先更新架构、任务边界和理由。
  • 页面高风险能力先真机取证并隔离测试,再接入完整流程。
  • 前序项目 cmroubao / cmpdd 只提供设计理由,不提供可直接复用的页面事实。