diff --git a/04-architecture.-.md b/04-architecture.-.md new file mode 100644 index 0000000..234e5b3 --- /dev/null +++ b/04-architecture.-.md @@ -0,0 +1,898 @@ + +> 同步来源:[`docs/04-architecture.md`](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/docs/04-architecture.md) · commit `afc651f75a3a` + +# 架构设计 + +## 一、系统结构 + +目标架构是一套后端服务、两个面向不同角色的客户端: + +```text +采购管理员 + | + v +管理 Web(服务端渲染) + | + v +统一 Backend API + | + +--> SQLite / 文件存储 + | + +--> Go 内存 ERP 会话 / 直连 source ----> 顺运宝 ERP + ^ + | +Android 采购 App --------------------> VLM Provider + | + +--> AccessibilityService + | + v + 拼多多 App +``` + +- 管理 Web 与 API 同属 `backend-api/`,但页面层不能直接访问数据库。 +- Android App 位于 `android-buyer/`,通过 API 领取任务和回传证据,在本地调用 AI。 +- 拼多多是第三方受控边界,只能由 Android 设备在已登录会话中操作。 +- VLM 配置和 Key 位于手机,Key 使用 Android Keystore 包装的加密存储;管理后端不 + 保存、下发或代理模型调用。 +- ERP 账号、验证码会话、Cookie 和可能的 JWT 仅存在于 Go API 进程的受锁内存会话; + 浏览器和 SQLite 只接收最小化规范货运数据。 + +## 二、模块职责 + +### 2.1 Backend API + +建议分层: + +```text +cmd/api/ # 进程入口、依赖装配、信号和优雅关闭 +internal/transport/http/ # Gin 路由、中间件、页面和请求/响应 DTO +internal/usecase/ # 创建、领取、状态迁移和结果归档 +internal/domain/ # 实体、状态机、权限和确定性约束 +internal/repository/sqlite/ # database/sql 仓储实现 +internal/platform/ # 配置、日志和文件存储 +``` + +职责: + +- 验证管理会话、采购人员和设备身份。 +- 创建任务并保存原始输入,不让模型结果覆盖原始事实。 +- 原子领取下一条任务,签发有期限的 claim lease。 +- 校验任务状态迁移、幂等键和设备归属。 +- 保存候选、事件、截图元数据和结果。 +- 为管理 Web 提供任务列表、详情和取消能力。 + +不得: + +- 根据 Web 按钮直接驱动某台手机点击。 +- 在 Gin handler 中写任务状态机、SQL 或仓储业务逻辑。 +- 保存拼多多密码、支付凭证或支付验证码。 + +T-201 已建立的基础包只有 `config`、`platform/database`、`platform/migration` 和 +`transport/httpapi`。HTTP 进程使用非零 read-header/read/write/idle timeout、 +1 MiB header 上限和 10 秒有界关闭;默认监听 `127.0.0.1:8080`,扩大监听范围必须 +显式配置。Gin 不启用默认 Logger/CORS/Recovery,异常恢复返回稳定 JSON 且不记录 +Authorization、Cookie 或 panic 内容。 + +SQLite 默认文件为被忽略的 `backend-api/var/cmroubao.db`,连接启用 foreign keys、 +5 秒 busy timeout、WAL 和 immediate transaction,并限制单连接以匹配单进程 MVP。 +数据库由 `cmd` 显式打开和关闭,不存在包级单例。Goose SQL 通过 `embed.FS` 加载, +`cmd/migrate` 显式执行 `up/down/status`。API 启动前检查所有迁移均已应用,不在 +服务进程内自动改表;T-203 已用 `00002_tasks_and_assets.sql` 建立任务、资产、事件 +和幂等记录表。 + +T-204 已用正式服务端 session 替换临时来源门禁,不重写 T-203 业务规则。Web 写 +操作校验浏览器 Cookie 与表单字段的随机双提交 CSRF token;Cookie 管理 API 写请求 +校验 `X-CSRF-Token`。明文 HTTP 只能监听 loopback,非 loopback 必须配置服务端 +TLS certificate/key。 + +### 2.2 Android App + +建议模块: + +```text +ui/ # 登录、任务列表、执行状态、设置 +taskclient/ # 后端 API、认证、幂等和重试 +tasksource/ # 统一任务源;本地探针与后端 API 实现 +workflow/ # ProcurementWorkflow 状态机 +automation/ # Accessibility 节点、截图、点击、输入、滑动 +ai/ # 结构化 AI、OpenAI 兼容适配器和本地安全配置 +evidence/ # 截图、步骤日志、脱敏和上传 +``` + +职责: + +- 检查无障碍、拼多多安装/登录可用性、网络和当前任务状态。 +- 由用户点击后领取任务;一台设备一次只运行一条。 +- 执行有界步骤:解析 -> 搜索 -> 浏览不超过 5 个候选 -> 判断 -> 停止确认。 +- 直接调用用户在本机明确配置的 VLM;没有可用模型时允许显式人工优先模式。 +- 显示当前步骤、停止原因和人工接管入口。 +- 用前台服务承载执行中任务,以服务端授权截止时间支持有限离线;进程重启后从加密 + 本地状态与服务端核对恢复或安全失败。 +- 回传步骤事件、候选、截图和最终结果。 + +不得: + +- 接收到通知就无条件开始采购。 +- 只用固定屏幕坐标定位关键控件。 +- 在验证码、登录、风险提示或未知页面上继续猜测操作。 +- 在 MVP 点击最终“提交订单”或支付相关控件。 + +### 2.3 VLM Gateway + +只负责三类能力: + +1. **需求提取**:参考图 + 标题 + SKU -> 类目、属性、置信度、警告;搜索词只用于 + 诊断和人工模式,不替代正式链路的参考图召回。 +2. **候选评估**:候选详情截图/文本 + 原始约束 -> 颜色/尺码硬约束结果、匹配项、 + 缺失项、拒绝原因、建议分。 +3. **候选规格兜底**:仅在候选发现阶段的规则定位/选中态确认失败时,把裁剪规格截图 + 与最多 200 个脱敏节点发送给本机 provider;定位结果只能引用当前快照 node ID, + 最终只读复核必须在同一快照同时覆盖颜色和尺码。 +4. **配置连通性测试**:设置页使用内置固定测试图对当前本机 provider/model 发出一次 + 多模态请求,只返回稳定成功或失败分类,不读取任务数据、不保存结果且不经过 Admin。 + +正式数据流固定为 `Android App -> VLM Provider`。管理后端只下发原始任务并接收 +结构化结果,不保存/下发 provider 配置或 Key,也不实现 `/tasks/{id}/ai/*` 代理。 +后台任务不能携带或覆盖本机 provider、Base URL、model、prompt 或 API Key。 + +App 支持两个显式模式: + +- `MANUAL_FIRST`:从原始标题/SKU 产生有界搜索词,由人员判断候选,不要求 VLM。 +- `AI_ASSISTED`:使用 App 本地配置的 OpenAI 兼容 provider 做需求提取和候选评估。 + +规格兜底仍服从 execution 的固定模式。自动领取时配置完整、安全且 provider 支持该 +能力才进入 `AI_ASSISTED`;否则进入 `MANUAL_FIRST` 并保持确定性候选流程。模式开始后 +任务 payload 和 Admin 均不能开启模型。配置中途失效时跳过无法确认的候选,不改写模式。 + +### 2.4 顺运宝 ERP 适配层 + +Go 直连 source 是外部系统防腐层,不属于采购任务状态机: + +```text +Admin 创建 sync run + -> Go 后台 worker 复用受锁 ERP 内存会话 + -> Go source 查询 listTotal/list/listByStock + -> Go source 规范化并去除收件 PII + -> Go 同事务 upsert freight order/items + -> Admin 复核 procurement request + -> 显式生成不可变 purchase task +``` + +- ERP 账号/密码只由 API 启动环境读取;浏览器不提交凭证,SQLite 和普通日志不保存凭证。 +- 精确单号只是查询条件,外部身份固定为 `stock.id` 和 `details[].id`。 +- source 不调用 Roubao/拼多多/VLM,不打印原始响应 body,只返回 allowlist 规范结构。 +- Go handler 只创建 sync run;外部查询由有界 worker 执行,避免把验证码或 ERP + 延迟绑定到浏览器请求。 +- 未确认的 `productThumb` 只按 ERP 引用保存,不能拼接 URL 或越权下载。 +- ERP 接口由页面协议观察得到,正式生产前需确认开放 API、服务账号、调用频率、 + 缓存和个人信息处理权限。 + +T-225 冻结了 `internal/platform/shunyunbao` 的协议常量、请求 header、完整单号/日期范围 +条件、分页、详情批量和 allowlist 归一化。T-226 增加受互斥保护的 Go 内存会话:Admin +以短期 ticket 读取验证码图片并人工输入验证码,服务端才持有 Cookie jar;账号密码只从 +启动环境读取。T-227 将此会话直接作为 `FreightSource`:每次同步先校验会话,再以最多 +100 条、每页 20 条和每批最多 100 个详情 ID 查询;响应不完整、身份冲突或会话失效均使 +整批失败。进程重启即失去会话,不用 Redis 或持久化 Cookie。货运用例只识别来源中立的 +“未配置、会话失效、未找到、协议异常、暂时不可用”错误。T-228 已删除旧 Python +Connector、loopback 端口和服务密钥;仓库运行时仅保留 Go API 进程。 + +模式在 execution 开始时固定并写入结果;AI 失败后只能由人员明确切换,不能静默降级。 +App 同时固定 provider ID、model、prompt/schema version 和证据 SHA-256,作为非秘密 +provenance 回传。Key、Authorization、完整 endpoint 和供应商原始响应正文不回传。 + +provider Key 从旧设置迁移到 Android Keystore 包装的加密存储;迁移或解密失败时 +拒绝真实调用。每台设备使用独立、可撤销、有限额度的 Key。Keystore 只降低静态泄露 +风险,不能宣称可抵抗已 Root 或完全受控设备。 + +模型输出是不可信建议,必须通过 schema 和确定性校验: + +- `sku`、`quantity` 和 `max_budget` 使用原始任务值;数量和预算不进入模型 prompt。 +- 原始 SKU 可唯一提取的颜色、尺码由本地代码固定为硬约束。候选必须对每个硬约束 + 返回 `MATCH/MISMATCH/UNKNOWN`;只有全部为 `MATCH` 才能进入自动 Top 5。 +- 价格未知、超预算或关键属性无法确认时不能判为可接受。 +- T-103 探针阈值为 `0.75`;低于阈值或标题/图片冲突时进入人工处理,不自动扩大 + 浏览范围。 +- 模型不能返回点击坐标、状态迁移或“允许提交订单”等执行授权。 + +T-104/T-211 按候选 ordinal 串行评估,每个候选最多一次结构化调用、整批最多 5 次;首个 +网络失败或无效输出停止后续调用。模型响应只允许 +`schema_version/candidate_index/decision/score/matched/missing_or_uncertain/ +rejection_reasons/confidence/hard_constraint_results`,其中 `decision` 仅允许 +`REVIEW/REJECT/MANUAL_REQUIRED`。`hard_constraint_results` 必须逐项原样回显本地给出的 +颜色和尺码期望值,并返回 `MATCH/MISMATCH/UNKNOWN` 与证据说明;缺项、重复、改写期望值 +或 `UNKNOWN` 均不能进入自动 Top 5。ordinal、证据 SHA-256、建议候选、provider provenance、 +`manual_review_required=true` 和 `order_submitted=false` 由本地代码确定。任务没有 +预算时,模型声称价格或预算匹配会被视为无效输出。 + +T-103 的模型响应只允许 +`schema_version/search_query/category/attributes/confidence/warnings`。属性来源限定为 +`TITLE/IMAGE/BOTH`;额外字段、无效 JSON、重复或越界属性、坐标/动作语义均视为无效 +输出并转人工。最终领域结果再由确定性代码补回原 SKU、数量、空预算、人工复核原因和 +`provider/model/prompt_version/reference_image_sha256`。 + +技术探针和后台 execution 的每个逻辑步骤最多发出一次 VLM 请求,不沿用通用 Agent +的重试循环。该结构化 HTTP 客户端关闭连接自动重试和 HTTP/HTTPS 重定向,整体调用 +超时为 75 秒;协程取消 +会取消底层 HTTP call,成功响应正文上限为 64 KiB,非成功响应不读取正文。 + +Release 远程 provider 只允许 HTTPS;HTTP 仅允许 Debug 下无 API Key 的 `localhost`、 +`127.0.0.1` 或 `::1` 本机 mock。端点不得包含 userinfo、query 或 fragment。标题 +和 SKU 在进入 +prompt 前分别限制为 2048 和 512 个 UTF-8 字节。JPEG 在调用前复核媒体类型、20 MiB +上限、魔数和 SHA-256,按声明长度一次分配并精确读取,Android 解码后最长边限制为 +2048 px。请求和响应正文、Base64、API Key 不写普通日志;Keystore-backed 密钥存储 +不可用且存在 Key 时拒绝调用。 + +设置页连通性测试沿用同一端点策略,禁用重试和重定向,调用超时 45 秒,成功响应限制 +为 64 KiB。固定测试图、Key、完整 endpoint 和供应商响应正文均不进入日志或持久化; +401/403、模型不可用、限流、服务端、网络、超时和响应异常只映射为稳定枚举。 + +## 三、核心数据流 + +### 3.1 技术探针 + +```text +本机私有蝦皮订单目录 + -> Debug 导入工具按订单号配对文本和参考图 + -> 规范化为 ProbeTask + -> Android FixtureTaskSource + -> 校验设备就绪 + -> 校验参考图并提取 SKU 颜色/尺码硬约束 + -> 将一次执行专用参考图写入受控 MediaStore + -> 通过拼多多可见图片搜索入口选择该图 + -> 检查图片结果页最多 5 个候选 + -> 过滤并排序颜色/尺码均确认匹配的 0..5 个候选 + -> 到达候选详情或确认页 + -> 停止并记录结论 +``` + +该阶段不依赖后台,用于尽早判断 Android 自动化是否可行。 + +图片搜索临时资产只保存匿名执行标识,不使用订单号或标题作为文件名。App 在写入前 +复核任务参考图的媒体类型、长度和 SHA-256,并在完成、失败或安全停止后 best-effort +删除自己创建的媒体。拼多多图片入口、相册页、当前图片或结果页无法唯一确认时停止; +不得从用户相册中猜选其他图片。图片搜索只负责召回,VLM 不能改变原始 SKU,也不能 +把 `UNKNOWN` 规格提升为通过。MediaStore 图片从 pending 发布后显式通知变更并等待 +750 ms 再启动拼多多,给系统媒体索引一个固定、有限的稳定窗口。 + +`TaskSource` 是工作流读取任务的唯一边界: + +- 第一层使用 `FixtureTaskSource`,消费 Debug 导入工具生成的本地私有样本。 +- 第二层替换为 `HttpTaskSource`,通过 Go-Gin 的 `claim-next` 和资产接口读取任务。 +- 两种实现必须产生相同的领域字段;workflow、AI 和 automation 不得判断任务来自 + 文件还是 HTTP。 + +Debug 导入工具运行在开发机上,Android App 不能假定能直接访问 Windows 绝对路径。 +工具读取用户显式配置的目录,将数据写入被 Git 忽略的 `.local/` 生成目录,再由 +Debug 构建或测试装载。导入必须: + +- 用文件主名关联订单号,不能用文本模糊匹配图片。 +- 首版严格解析 UTF-8 四行标签 `店铺名:`、`商品标题:`、`SKU:`、`数量:`。 +- 校验店铺名、标题、SKU、正整数数量和唯一且可解码的 JPEG 参考图。 +- 对缺失、重复、编码无法识别和字段格式未知返回结构化错误。 +- 不把完整订单号、店铺名、原图路径写入普通日志。 +- 仅在用户明确配置 VLM 后发送必要的标题、SKU 和参考图;不发送订单号或店铺名。 +- `RequirementProbeSource` 只从固定 asset 根读取已导入任务和引用图片;随后立即进入 + 大小、JPEG 魔数和 SHA-256 复核,不接受任意 cache 或绝对路径。 + +实现边界: + +- `task-contract/`:纯 Kotlin `ProbeTask`、`TaskSource` 和版本化 JSON codec。 +- `tools/shopee-importer/`:开发机 CLI,严格校验并生成匿名资产名。 +- `app/src/debug/.../FixtureTaskSource`:只在 Debug 代码中读取 + `assets/probe-fixtures/tasks.json`。 +- `ProbeWorkflowCoordinator`:只依赖 `TaskSource`,未来可无缝替换 `HttpTaskSource`。 + +私有 fixture 只有在构建显式设置 `-PprobeFixturesDir` 时注入 Debug APK;普通构建 +必须同步清空生成资产,防止上一次真实样本残留。 + +T-101 的固定词搜索已落在以下运行边界: + +```text +SearchProbeScreen + -> WorkflowRunner(timeout / retry / stop) + -> PinduoduoSearchAutomation(纯 Kotlin) + -> AndroidPinduoduoUiDriver + -> BuyerAccessibilityBridge + -> BuyerAccessibilityService + -> 拼多多可见、启用、唯一语义节点 +``` + +结果页成功不是“点击已发送”,而是同时确认精确查询词、搜索结果头和至少 3 个排序 +控件。`ACTION_SET_TEXT` 后必须重新读取当前根节点并精确比对输入;任何稳定未知页以及 +登录、验证码、风控、订单或支付边界都终止 workflow。该路径不提供坐标、ADB、 +Shizuku shell、OCR 或 VLM 动作降级。 + +T-102/T-104 在搜索结果后追加一个有界候选步骤: + +```text +精确匹配固定词或当前结构化需求词的结果页 + -> 最多 2 次滚动预算 + -> 最多 5 个去重商品卡 + -> 验证详情页 + -> 详情页无障碍截图 + -> 受控 DISCOVERY_INSPECT 规格入口 + -> 选择目标颜色/尺码并读取组合价格 + -> 规格弹层语义和截图 + -> App cache/candidate-NN-{detail,specification}.png + manifest.json + -> 关闭规格弹层并复核详情页 + -> 一次全局返回并复核固定词结果页 +``` + +候选卡只保留语义指纹和计数,详情/规格截图保存在 App 内部 cache。manifest v4 记录 +匿名文件名、卡片/详情语义 SHA-256、截图 SHA-256、字节数、尺寸、有界规格语义、 +已选摘要、严格组合价格和 best-effort 页面可见标题;不保存完整页面原文。标题最多 +160 字,动作、价格和纯数字文本不会作为标题。T-104/T-213/T-214 使用这些证据时 +必须通过受控 evidence 边界读取,不能让 VLM adapter 自行遍历 cache。Android +10/API 29 及以下不能运行当前截图探针,应在预检时明确不支持,不使用媒体投影或 +shell 绕过。 + +T-274 将候选的标题语义 signature 与点击身份分离。每次结果树读取为已接受卡片生成 +只在当前视口存活的 locator,包含 recycler child index、接受顺序、bounds、点击目标 +相对 path 以及完整语义/节点结构 SHA-256;不保留原始节点或原始文本。打开前重读树, +同时复核 signature、内容指纹和位置,只有唯一命中才对节点执行 `ACTION_CLICK`;位置 +变化时只允许唯一指纹加 80% bounds 重叠重定位。locator 不写 manifest、进度、outbox +或 Admin。候选尝试去重使用不含位置的完整内容指纹,因此同标题不同卡片可分别尝试, +同一完整卡片滚动后不会反复尝试。初次候选需连续两次读取产生相同 locator stability +keys(含位置与节点指纹)。 + +`CandidateEvidenceSource` 只接受当前 workflow 内存中的连续 ordinal 元数据,文件名 +固定为 `candidate-01-detail.png`/`candidate-01-specification.png` 至第五组;每张 +PNG 最多 8 MiB、全批最多 64 MiB、声明尺寸最长边不超过 10000 px,并复核规范路径、 +PNG/IHDR、精确字节数、尺寸和 SHA-256。每个候选回传时必须恰好绑定 +`DETAIL`/`SPECIFICATION` 两份证据,生成独立 asset ID;先验证全批 ordinal、类型和 +哈希再写入加密 outbox,候选的 asset ID 数组固定按 `DETAIL`、`SPECIFICATION` +排序,重试沿用各自 idempotency key。Android gateway 再解码详情图并把最长边缩至 +2048 px。当前需求快照与搜索词必须同时匹配候选 session,否则拒绝评估;另一个 +关键词的结果页只允许重新进入搜索框,不能采集候选。 + +T-213 规格核验优先使用“选择规格/请选择规格/颜色分类/颜色款式/选择颜色/选择尺码” +或“已选/请选择”前缀的唯一入口;拼多多 8.17 没有直接入口时,`DISCOVERY_INSPECT` +允许从已验证详情页点击唯一“免拼购买”语义目标。弹层最多读取 160 个语义节点、 +每组最多 30 个选项,目标颜色/尺码必须各自唯一、启用且每次点击后可复核选择状态。 +选定后读取明确人民币组合价格;重复、缺组、禁用、滚动截断、状态未变化、多价格冲突 +或无障碍语义不足一律 `UNKNOWN/MISMATCH`,不得继续到订单提交。 + +候选评估 schema/prompt v3 把本地规格结果作为权威输入;模型返回的颜色/尺码状态必须 +逐项与本地结果相同,否则整批降级人工检查。详情和规格证据 SHA-256 同时绑定 +assessment、重排身份和 outbox 资产,VLM 不能通过主图推断覆盖本地硬约束。 + +T-276 将后台采购候选搜索收敛为两轮。第一轮最多打开 6 个不同结果并保存最多 5 个 +`EXACT_MATCH`;达到 5 个后直接封板。未达到目标时才重新导入参考图执行第二轮,第二轮 +只检查默认排序前 2 个不同商品并以 `FALLBACK` 保存,累计快照最多仍为 5 个。每轮浏览 +超时默认 `100000/45000` 毫秒,加图片搜索步骤后的工作流总预算为 245 秒。新 execution +不得产生第三轮;升级前已写入数据库的第三轮观察保持只读审计,不迁移、不改写。 + +T-272 在上述候选规格读取内增加有界兜底,而不改变订单 dry-run:采集侧同时聚合节点 +自身或四层内祖先的 clickable/selected/checked;规则给出唯一结果时零模型调用。仅对 +可见信息不完整、重复歧义、点击失败或点击后选中态未知构造快照,节点最多 200 个、 +深度最多 12、文字最多 160 字符,并在截图前后复核结构 hash。定位输出经严格 schema、 +`0.92` 阈值、node ID、path 和局部指纹复核后只点击一次;颜色、尺码串行处理。最后的 +`0.95` 双规格视觉证明独立保存在规格证据中并绑定实际保存的截图 hash,不伪造节点 +selected 状态。每候选最多两次定位和一次最终复核;第二轮保底不启用。 + +T-277 将最终规格复核 schema 升级为 v2。当颜色/尺码已经选中但“已选择”摘要或唯一组合价 +缺失时,也允许使用剩余的一次最终复核调用。模型只能返回当前快照中的颜色、尺码和最多 +4 个价格 node ID;App 重新确认结构 hash,按屏幕位置拼接这些节点原文并使用共享人民币 +解析器生成分值。模型自报文字与节点拼接不一致、未知/重复节点、低于 `0.95`、旧快照或 +无法解析的金额全部拒绝。接受结果将 SKU 摘要、价格、proof 和 `LOCAL_VLM` 来源绑定同一 +规格截图 SHA-256。候选 outbox、v21 observation 和 Admin 显式保存/展示 SKU +`MATCH|MISMATCH|UNVERIFIED`、价格 `VERIFIED|UNVERIFIED` 与值来源;第二轮保底固定为 +两个 `UNVERIFIED`,不调用模型。 + +T-281 修正规则层过早结束首轮规格采集的问题。规则点击后必须复读选中态;点击未生效、 +完整分组因别名无法定位目标或结果歧义时,允许在同一候选内进入上述 node-ID VLM 兜底, +明确 disabled 的目标仍直接失败关闭。每次模型调用独立限制为 20 秒,异常、超时、合同 +不符、低置信度、节点/证据/价格无效、旧快照和点击拒绝使用稳定原因码。无价格上限的 +候选仍尽力读价,但缺价本身不再消耗模型调用或否决 SKU;确定性解析额外允许固定标签 +“拼单价/单买价/券后价/券后/到手价/活动价/优惠价/折后价/补贴价/限时价”加唯一金额, +区间、“起”、未知标签和多个金额继续保持未验证。 + +采购运行证据写入 App `noBackupFilesDir` 下的私有 SQLite,并合并展示在“记录”模块。 +每个 execution 保存规则判定、模型阶段/稳定终态、节点摘要、合法白名单响应摘要和候选 +SKU/价格状态;最多保留 50 次运行、每次 200 个事件。截图/Base64、Key、token、Cookie、 +地址和完整任务描述不得入库;不符合严格 schema 的响应只留 SHA-256 与长度。后台只接收 +有界的模型拒绝原因聚合,不接收节点 JSON、模型响应或图片。 + +PKG110 上的拼多多 8.17.0 服装详情只暴露不可点击的“颜色/款式”预览;完整颜色/尺码 +和组合价格位于“单独购买/免拼购买”后的规格弹层。业务方已确认最终流程需要自动创建 +待付款订单,因此 T-213 重新定义两级能力中的第一层:`DISCOVERY_INSPECT` 可以从 +已验证详情页进入规格弹层并选择目标颜色/尺码以读取组合价格,但不能加入购物车、 +提交订单或触发支付。后续 `ORDER_CREATE` 必须由 Admin 对确定候选签发一次性授权, +使用独立状态机和幂等/对账边界,不能复用候选探查权限。 + +`DISCOVERY_INSPECT` 从任务 SKU 的本地规范化结果取得唯一颜色/尺码,按颜色后尺码 +的固定顺序选择,每次动作后重新读取 selected 与“已选择”摘要;规格区域最多向前 +滚动 2 次。确定性价格只接受单一、完整的人民币文本,或 T-281 固定白名单价格标签后的 +唯一人民币文本;T-277 允许本机模型从同一快照选择拆分的币种和数字节点,再由代码按 +相同规则解析。普通对照促销价、区间、未知折扣条件、多实际价格冲突或无价格一律保持 +未验证。 +若唯一入口落到订单确认页,自动化只能 +执行系统返回并跳过该候选。 + +评估结束后 automation 不再产生动作,UI 进入 `AWAITING_CONFIRMATION`、 +`MANUAL_REVIEW` 或 `NO_MATCH`。人员可以把本地建议项标记为可用,或拒绝本次候选; +这只是验证结果,所有状态的 `order_submitted` 均为 `false`。 + +需求提取是独立探针,不启动拼多多,也不接入 `MobileAgent`。GUI-Owl 和 MAI-UI 输出 +动作/坐标,明确不具备 `supportsRequirementExtraction` 能力。真实供应商未确认前, +普通测试只使用 Fake gateway;本机 OpenAI 兼容 mock 仅验证 Android 请求链路和隐私 +边界,不作为真实模型效果证据。 + +### 3.2 MVP 业务闭环 + +```text +管理员创建 PENDING 任务 + -> 采购员在 App 点击“获取任务” + -> 后端事务内选取并置为 CLAIMED + -> App 确认后置为 RUNNING,取得有限离线授权 + -> best-effort heartbeat 同步步骤和取消 + -> App 本地需求提取和拼多多自动化 + -> WAITING_CONFIRMATION + -> 采购员接受/拒绝候选 + -> 加密 outbox 幂等回传结果和证据 + -> SUCCEEDED 或 FAILED/CANCELED + -> 管理端展示结果 +``` + +执行事件与后台共享单条 `message <= 1000 UTF-8 bytes` 契约。候选搜索的状态和聚合 +诊断在同一个 EVENTS outbox 请求中使用两条事件,不靠截断丢失诊断;T-274 已生成但被 +后台整体拒绝的旧超长候选事件,会在上传边界按 ` cards=` 稳定拆分,并从原 event ID +确定性派生诊断 event ID,保证网络重试的请求体不变。 + +outbox 严格按队首顺序 flush。连接、TLS、DNS、超时或可重试 5xx 保留队首并在授权窗口 +内重试;后台已经响应的不可重试错误只保存错误码、HTTP 状态和白名单校验字段,停止 +同步及后续拼多多动作,不能越过、丢弃或伪装成后台离线。响应正文和字段错误内容不进入 +本地持久化、UI 或日志。 + +## 四、状态机 + +### 4.1 任务状态 + +```text +PENDING + | claim + v +CLAIMED ---- lease expired/release ----> PENDING + | start + v +RUNNING + | candidate ready + v +WAITING_CONFIRMATION + | accept validation result + v +SUCCEEDED + +PENDING/CLAIMED/RUNNING/WAITING_CONFIRMATION + +--> CANCELED(仅允许规则规定的主体取消) + +CLAIMED/RUNNING/WAITING_CONFIRMATION + +--> FAILED(结构化错误) +``` + +规则: + +- `SUCCEEDED` 是验证结果成功,不代表已下单;`order_submitted=false`。 +- `CLAIMED` 默认租约为 10 分钟;`RUNNING/WAITING_CONFIRMATION` 默认授权为 + 30 分钟,允许范围 5 至 120 分钟。App 每 30 秒 best-effort heartbeat,成功后 + 按服务端时间滑动续期。 +- 只有当前设备和有效 claim token 能启动、续租、上报或结束任务。 +- 终态不可回退;重试创建新的 execution attempt,不篡改历史证据。 +- App 在 claim 前生成并安全保存 256 bit Raw URL token;后端只存 SHA-256, + token 与 `Idempotency-Key` 相互独立。 +- 过期 `CLAIMED` 可以原子回收;过期 `RUNNING/WAITING_CONFIRMATION` 保持原状态, + 不自动回队列。App 到达服务端授权截止时间持久化 `SAFE_STOPPED` 并停止前台 + 执行;原设备重连 heartbeat 只同步状态且不延长已过期授权。T-207 才允许原设备 + 补报授权内已产生的终态结果并留下过期补报审计标志。 +- 管理取消 `PENDING/CLAIMED` 立即终态;执行中只设置停止请求,App 在下一个安全 + 检查点停止并调用 `cancel-ack` 后才进入 `CANCELED`。离线期间不能保证即时取消, + App 恢复连接后必须先同步取消状态再继续页面动作。 + +T-206 Android 使用独立 `ProcurementSecureStore` 保存 backend URL、BUYER access +token、设备 secret、claim token、各操作幂等 key、任务快照和 execution。存储创建 +失败时整个后台采购入口 fail closed,不回退普通 `SharedPreferences`。登录密码只 +存在于当前 Compose 输入和一次请求中;后台任务不能读取或修改 Roubao provider。 +`HttpTaskSource` 只把已领取任务和经过 MIME/JPEG/大小/SHA-256 校验的内部参考图 +映射为现有 `ProbeTask`,不参与领取策略。 + +### 4.2 Android 工作流状态 + +```text +IDLE + -> PREFLIGHT + -> REQUIREMENT_ANALYSIS + -> OPEN_PDD + -> SEARCH + -> SCAN_RESULTS + -> REVIEW_CANDIDATE + -> WAITING_USER + -> FINISHED + +任意执行态 -> STOPPING -> FAILED/CANCELED +``` + +每一步必须有: + +- 进入条件和预期页面特征。 +- 最大等待时间和最大重试次数。 +- 可观察事件。 +- 允许的下一步。 +- 安全停止条件。 + +## 五、数据模型 + +所有主键使用服务端生成的 UUID;时间统一为 UTC 的 ISO 8601。 + +### `users` + +| 字段 | 约束 | 说明 | +| --- | --- | --- | +| `id` | PK | 用户 ID | +| `username` | UNIQUE, NOT NULL | 登录名 | +| `password_hash` | NOT NULL | 不保存明文 | +| `role` | NOT NULL | MVP 为 `ADMIN` 或 `BUYER` | +| `is_active` | NOT NULL | 禁用后现有和新凭证均不可使用 | +| `created_at` | NOT NULL | 创建时间 | + +### `devices` + +| 字段 | 约束 | 说明 | +| --- | --- | --- | +| `id` | PK | 设备 ID | +| `name` | NOT NULL | 人类可识别名称 | +| `token_hash` | NOT NULL | 设备令牌哈希 | +| `bound_user_id` | FK, nullable | 当前绑定采购员 | +| `app_version` | nullable | App 版本 | +| `android_version` | nullable | Android 系统版本 | +| `pdd_version` | nullable | 已验证拼多多版本 | +| `last_seen_at` | nullable | 最近心跳 | +| `readiness_reported_at` | nullable | 最近一次就绪上报的服务端时间 | +| `accessibility_enabled` | NOT NULL | 无障碍连接就绪位 | +| `pdd_installed` | NOT NULL | 拼多多安装就绪位 | +| `is_enabled` | NOT NULL | 后端开关 | + +设备必须先由本地管理命令预授权。首次 BUYER 联合登录可以把 `bound_user_id` 为空的 +设备原子绑定给当前采购员,不支持客户端自助重新登记或抢占其他采购员的设备。 + +### `admin_sessions` / `access_tokens` + +- session/access token 都使用至少 256 bit 的随机 opaque secret,原值只通过 + `Set-Cookie` 或登录响应返回一次,数据库只存 SHA-256。 +- 管理 session 和 App access token 均固定30天绝对有效,不因请求、heartbeat 或任务 + 执行滚动续期。两类凭证继续隔离;既有 session/token 保留签发时的数据库到期时间。 +- 每次鉴权都联查用户/设备启用状态,因此禁用立即生效,不等待 token 到期。 +- 管理 Cookie 与 App Bearer token 使用不同 middleware,不能互相替代。 +- T-203 的 `local-admin` 继续作为单管理共享资源 scope;真实 `user_id` 另作 actor, + 避免切换身份后隐藏已有任务。 +- 管理和 App 登录在 bcrypt 前共享一个内存有界限流器,但使用独立 scope + 服务端 + `RemoteAddr` 键;成功登录清零,超限返回 `429` 与 `Retry-After`。 + +### ERP 货运来源层 + +| 表 | 主要内容 | +| --- | --- | +| `erp_sync_runs` | 查询模式、匿名查询 hash、时间范围、状态、计数、错误码和成功水位 | +| `freight_orders` | creator、source system、external stock id、来源单号、店铺、状态、ERP 时间、canonical hash/revision | +| `freight_order_items` | freight order、external item id、标题、规格、SKU、数量、图片引用、采购状态、canonical hash/revision | +| `procurement_requests` | 货运明细 revision 的不可变采购字段、校验状态、参考图和来源变化状态 | +| `purchase_task_sources` | procurement request/revision 与现有 purchase task 的幂等关联 | + +货运层只保存采购所需字段,不保存 receiver、receiverTel、receiverAddr、Cookie、JWT +或完整原始 JSON。重复同步按外部 ID upsert;内容 hash 不变时不增加 revision。 +来源变化只产生新 revision/状态,绝不修改已生成的采购任务。 + +一个货运单可以有多个商品明细;一个采购任务仍保持单商品、单 SKU、单参考图。 +缺少明确标题/SKU/正整数数量/有效参考图的请求停在 `NEEDS_REVIEW/NEEDS_IMAGE`。 + +### `purchase_tasks` + +| 字段 | 约束 | 说明 | +| --- | --- | --- | +| `id` | PK | 任务 ID | +| `created_by_user_id` | FK, nullable | 新任务的真实 ADMIN actor;历史任务允许为空 | +| `title` | NOT NULL | 原始标题 | +| `description` | NOT NULL | 原始说明,可为空字符串 | +| `sku` | NOT NULL | 原始 SKU,不得由模型改写 | +| `image_asset_id` | FK, NOT NULL | 原始参考图 | +| `quantity` | `> 0` | 权威数量 | +| `max_budget` | `> 0`, nullable | 全部数量的权威最高商品总预算,币种为 CNY | +| `status` | NOT NULL | 任务状态枚举 | +| `claimed_by_user_id` | FK, nullable | 当前采购员 | +| `claimed_by_device_id` | FK, nullable | 当前设备 | +| `claim_generation` | `>= 0` | 每次领取递增,释放后保留用于审计 | +| `claim_token_hash` | nullable | 64 位小写 SHA-256;永不保存原 token | +| `claim_issued_at` | nullable | 当前 claim 签发时间 | +| `claim_expires_at` | nullable | 租约到期时间 | +| `cancel_reason` | nullable | 管理取消原因 | +| `cancel_requested_at` | nullable | 执行中停止请求时间 | +| `cancel_requested_by_user_id` | FK, nullable | 请求停止的 ADMIN | +| `canceled_at` | nullable | 取消终态时间 | +| `version` | NOT NULL | 乐观锁/状态并发控制 | +| `created_at/updated_at` | NOT NULL | 审计时间 | + +### `task_events` + +- 事件只追加;T-205 已实现 `TASK_CREATED`、`TASK_CLAIMED`、 + `TASK_RECLAIMED`、`TASK_RELEASED`、`TASK_STARTED`、 + `TASK_CANCEL_REQUESTED`、`TASK_CANCELED`。 +- `actor_user_id` 是指向 `users` 的可空外键。新管理操作必须写入真实 ADMIN, + T-203 历史事件保持为空。 +- `actor_device_id` 是指向 `devices` 的可空外键;App 状态迁移同时记录用户和设备。 +- 设备/任务 heartbeat 不写事件,避免高频审计膨胀。 +- 管理任务详情 API 可返回 actor;密码、session、设备 secret 和 access token 永不 + 进入事件。 + +### `task_executions` + +| 字段 | 约束 | 说明 | +| --- | --- | --- | +| `id` | PK | 一次执行尝试 | +| `task_id` | FK | 所属任务 | +| `attempt_no` | UNIQUE(task, no) | 尝试序号 | +| `claim_generation` | UNIQUE(task, generation) | 对应的 claim 代次 | +| `device_id/user_id` | FK | 执行设备和人员 | +| `current_step` | 1-64 bytes | 最近 heartbeat 的执行步骤 | +| `last_heartbeat_at` | NOT NULL | 最近运行 heartbeat | +| `order_submitted` | NOT NULL, false | MVP 数据库约束必须为 false | +| `started_at/finished_at` | started 必填 | 执行耗时和安全结束 | + +候选、模型派生结果、outcome、结构化失败和 execution evidence 属于 T-207,不在 +T-205 的最小 execution 表中提前伪造。 + +### `lifecycle_requests` + +- 主键为 `user_id + device_id + operation + idempotency_key`。 +- operation 为 `CLAIM_NEXT`、`START`、`RELEASE`、`CANCEL_ACK`;保存请求 SHA-256 + 与 task/generation/execution 结果引用。 +- `NO_TASK` 也是稳定结果;同 key 重放不会因后来新增任务而改变。 +- claim 原 token 不进入该表,请求 hash 只包含 token hash。 + +### `assets` 与后续 `execution_events` + +- T-207 的 `execution_events` 只追加,记录 step、事件类型、可读消息和时间;不保存 + 密码或 token。 +- `assets` 保存文件相对路径、媒体类型、大小、哈希、创建者和保留时间。 +- 原图和截图必须通过鉴权接口读取,文件名不能直接作为公开 URL。 +- 任务参考图上传可接受 JPEG/PNG/WebP,但后端必须先真实解码、限制字节与像素,再 + 统一规范化为匿名 JPEG;未来 Android 读取的资产不能依赖原始扩展名或声明 MIME。 + +### 候选决策数据集(T-208) + +T-208 已在 T-207 的候选、事件、截图和最小结果回传基础上,于 +`task_executions` 下增加 +独立、不可变的数据层,不把整批候选塞进 `task_events`、`purchase_tasks` JSON 或 +一段不可查询的自由文本: + +| 表 | 主要内容 | +| --- | --- | +| `candidate_search_runs` | execution、需求快照 hash、搜索词、App/拼多多版本、开始/结束时间 | +| `candidate_observations` | run、ordinal、可选平台商品 ID/规范化 URL、可见标题/规格/价格、采集状态和截图 asset | +| `candidate_observation_identities` | execution-scoped candidate key、原 ordinal、卡片/详情语义签名、详情/规格证据 hash 和 identity 版本 | +| `model_runs` | provider/model、prompt/schema/阈值版本、耗时、token/成本和请求/结果 hash | +| `candidate_evaluations` | observation/model run、decision、score/confidence、matched/missing/rejection reasons | +| `candidate_recommendations` | run、推荐 observation、conclusion、确定性策略版本和简短理由 | +| `candidate_human_reviews` | run、最终 outcome、选择项、actor、理由 schema、时间和可空 `supersedes_review_id` | +| `candidate_human_review_items` | review 下每个候选的 `ACCEPT/REJECT`、主要理由和可选备注 | +| `candidate_human_review_reasons` | review item 的多值结构化理由码 | + +四层数据语义不可混用: + +- observation 是 App 当时实际看到的页面事实。 +- prediction 是特定模型版本的输出。 +- recommendation 是本地确定性规则的结果。 +- human label 是采购人员明确提交的结论,才可作为离线准确率标签。 + +现有 `assets.purpose` 在 T-207/T-208 migration 中按实际需要扩展 +`CANDIDATE_SCREENSHOT`、`CANDIDATE_IMAGE` 和 `EXECUTION_EVIDENCE`。第三方商品或 +图片 URL 只保存为长度受限、规范化的辅助观测值;后端不得盲目请求该 URL。主要证据 +必须是 App 上传后由 assetstore 校验、脱敏和受鉴权访问的时间点快照。 + +T-214 后,v7 以后写入的每个 observation 在同一事务内生成 +`candidate_observation_identities`。`candidate_key` 使用版本域、execution ID、原始 +ordinal、详情语义签名和规格证据 hash 生成,只标识该次 execution 的持久 observation, +不冒充拼多多全局商品 ID。App 按 `DETAIL`、`SPECIFICATION` 顺序提交两个不同的 +evidence asset ID;后端逐一核对其实际 SHA-256 后才接受候选。Admin API/Web 展示 +key、四个指纹和受控截图;后续选品授权必须引用 key 并绑定任务内容 hash 和 review +版本,不能只信 URL 或列表 ordinal。 + +### Admin 下单授权(T-215) + +非空候选保存后,任务从 `RUNNING` 进入 `WAITING_CONFIRMATION`。Admin 选择不会调用 +Android 或拼多多,而是在后端追加一版 human review 和一条 +`order_authorizations`: + +```text +Admin task detail + -> candidate key + 全候选结构化理由 + -> 校验 task/execution/content hash/version + -> candidate key 映射原 observation + -> 追加 admin human review + -> 生成 PENDING_DELIVERY authorization 快照 + -> T-216 device command pull +``` + +授权快照固定原始任务 SKU/数量、候选规格/价格、review 版本和 T-214 四个身份指纹; +同时冻结任务最高总预算或后台默认 200 元低价总额上限及来源。候选展示价只保留为观察 +数据,不再作为当前规格价格的等值授权条件。价格上限、来源和 schema v3 都参与命令哈希。 +请求不能覆盖这些字段。一次 execution 最多一条 active 授权;设备领取前改选只能 +追加新版本并把旧记录标记为 `SUPERSEDED`,设备领取后必须走显式停止/撤销协议。 +浏览器不持有设备 claim token,授权表也不保存拼多多登录、支付或收货凭证。 + +### 设备命令投递(T-216) + +Roubao 复用前台服务的 30 秒 heartbeat 主动 pull,不依赖 Android 后台推送: + +```text +flush candidate outbox + -> task heartbeat keeps lease + -> POST commands/next + -> backend validates claim + execution + active authorization + -> PENDING_DELIVERY -> DELIVERED and returns canonical command + -> App validates and saves encrypted PendingOrderCommand + -> POST commands/{id}/ack with same hash/idempotency key + -> DELIVERED -> ACKNOWLEDGED +``` + +HTTP 响应不确定时,服务端对 `DELIVERED/ACKNOWLEDGED` 重放相同 command 和 hash; +App 只有在加密状态提交成功后才 ACK。投递/确认与真正页面执行分离,T-216 收到命令 +后仍不打开拼多多。App 本地旧候选选择不再作为后台任务完成条件。 + +### 已授权订单 dry-run(T-217) + +```text +ACKNOWLEDGED command + -> encrypted PendingOrderDryRun + -> idempotent dry-run start: authorization -> EXECUTING + -> open authorized canonical Pinduoduo product + -> normalized title + current goods_id / authorized detail fingerprint / + bounded current-share goods_id verification + -> deterministic color/size/price first + -> bounded local VLM node-ID fallback when deterministic evidence is incomplete + -> quantity + actual-price/frozen-cap verification + -> unique specification confirmation action + -> ORDER_CONFIRMATION read-only verification and evidence + -> encrypted READY + -> idempotent backend ready record +``` + +旧 ordinal 和第三方 URL 都不参与点击。旧截图 hash 是 Admin 决策证据,不要求新截图 +逐像素相等;重新定位至少精确命中一个旧语义签名,并同时满足标题、SKU、实际价格与唯一性。 +规格单价必须唯一可读,单价乘数量必须等于确认页总额且不超过授权冻结上限;App 与后台 +各自校验一次。候选价格为空不阻断,但实际价格为空或歧义仍失败关闭。 +App 可以从规格弹层进入确认订单页,但 T-217 的 accessibility action allowlist 不包含 +确认订单页的提交、付款、地址或优惠选择。READY 同时存在于手机加密状态与后端审计表, +才允许 T-218 消费。 + +T-279 将 direct dry-run 的瞬时自动化尝试限制为两次。身份、SKU、价格或预算确定性失败 +不会继续操作拼多多:App 先把稳定 failure code/message 和幂等键写入加密状态,再重传 +failure API;网络恢复只重传失败结果,不重跑页面动作。后台把授权置为 `FAILED`,任务仍 +处于 `WAITING_CONFIRMATION` 以便 Admin 重新选品。成功、失败和安全停止继续复用前台返回 +协调器切回 Roubao。 + +本机 VLM 只在 Roubao 模型配置通过安全校验后启用,不依赖候选阶段的 execution mode。 +每次 dry-run 最多两次规格 node-ID 定位和一次最终双规格/价格复核;点击前复核同一快照 +结构,点击后重取无障碍根节点。模型无权确认订单、提交订单或点击支付。 + +拼多多 8.17 的结算 WebView 偶尔只暴露外层节点。此时 App 可对无障碍服务捕获的当前 +确认页截图运行 bundled ML Kit 中文 OCR,但它只返回只读证据:标题必须满足有界锚点、 +颜色/尺码必须通过同一 SKU 别名匹配器、数量必须位于规格与优惠边界之间,实付款必须 +唯一可解析。任一条件不满足即停止,OCR 层没有点击接口。 + +### 单次订单提交与回读(T-218) + +```text +local READY + backend READY + valid lease + -> revalidate confirmation/address/manual-payment boundary + -> idempotent backend submission fence (authorization becomes non-repeatable) + -> persist encrypted CLICK_ASSUMED + -> at most one exact "提交订单" accessibility action + -> never click payment; navigate read-only to order list + -> unique pending-order reconciliation + -> RECONCILED or MANUAL_REVIEW, never a second submit +``` + +submission fence 是 exactly-once 安全边界,不是“点击成功”回执。服务端创建围栏后, +任何进程恢复都假定订单可能已创建;宁可留下需要人工对账的未提交意图,也不能通过 +重按产生重复订单。一个 authorization 和 dry-run 在数据库中只能对应一个 submission。 +只有同一次未中断调用中新建本地 intent 并收到后台 `FENCED` 响应时才短暂持有点击 +资格;磁盘恢复出的 intent 只能重放后台请求并进入对账。围栏已经存在后,即使 claim +租约到期也只继续对账,不恢复商品操作或提交资格。 + +不可逆动作前再次核对 T-217 全部商品字段,并要求明确可见的已配置地址和唯一 +“提交订单”节点。地址缺失、金额变化、自动扣款方式或“立即支付/确认支付/免密支付/ +先用后付”语义均阻塞。OCR 和 VLM 只能提供只读证据,不能决定或发出提交。 + +点击后只读检查有限数量的最近/待付款订单。归属必须由订单编号标签、平台下单时间、 +待付款状态、标题、SKU、数量和金额共同唯一确定;零个或多个匹配转人工。订单号和时间 +只进入 App 加密状态与鉴权后端,不进入普通日志或模型请求。T-219 读取该对账记录展示 +待付款提醒,所有支付仍由采购人员在拼多多完成。 + +### 待付款提醒(T-219) + +Admin 任务详情从同一 `TaskDetail` 聚合读取 v11 submission,不按浏览器传入的订单号 +查询。`FENCED`、`MANUAL_REVIEW` 和 `RECONCILED` 分别映射为正在对账、人工对账和 +待人工确认付款;只有最后一种展示订单号、平台时间、金额和对账证据。受控截图继续走 +现有鉴权 evidence 路由,订单号不进入 URL。Roubao 从本地加密 submission 展示同一 +待付款摘要,不增加支付导航。旧 `SUCCEEDED` 的“未提交订单”文案只有在不存在 +reconciled submission 时保留。 + +人工理由使用版本化 allowlist。接受或拒绝至少有一个理由且指定主要理由; +`OTHER` 才要求 4-200 字备注。拒绝推荐后改选必须同时产生一条原推荐项负标签和一条 +替代项正标签;全部无匹配时每个曝光候选都有负标签。人工修正追加新 review 并引用 +旧 review,不能覆盖历史。模型理由可以展示给人员参考,但不能自动成为人工理由。 + +## 六、API 和并发边界 + +- 合约以 [`api.md`](/chengma/mroubao/wiki/api) 为准。 +- `claim-next` 必须由 usecase 调用 repository,在一个数据库事务内完成 + “幂等检查 + 就绪/单活校验 + FIFO 选取/过期回收 + 更新 + 事件”。 +- 当前设备若有自己的过期 `CLAIMED`,优先原子回收该任务;没有时才在全局候选中 + 按 `created_at ASC, id ASC` 选择。该例外保持设备唯一归属并避免静默清理审计状态。 +- SQLite DSN 使用 `_txlock=immediate`、WAL 和 busy timeout;跨两个独立数据库连接 + 的并发测试必须精确得到一个领取者。 +- claim/start/release/cancel-ack 支持 `Idempotency-Key`;heartbeat 不需要幂等表。 +- App 对网络超时不能假定失败;必须用任务详情确认服务端最终状态。 +- 后端拒绝非法状态迁移,即使客户端 UI 隐藏了对应按钮。 +- App 参考图通过 task-scoped URL 读取;每次校验当前 user/device/generation/token/ + 租约,不允许用可猜测 asset ID 越权读取其他任务。 +- SQLite MVP 使用单后端进程;切换多实例前先迁移 PostgreSQL 并验证领取竞争。 +- Gin handler 只做鉴权上下文、binding、调用 usecase 和响应映射。 + +## 七、自动化与安全边界 + +- 只允许目标拼多多包处于前台时发送动作。 +- 关键动作前后都校验包名、页面特征和任务取消标志。 +- 节点选择要求可见、启用且唯一匹配;优先可访问性语义、稳定文本和受限祖先层级, + 不能因资源 ID 重复而任取第一个节点。 +- 已验证的首页、搜索输入和结果页路径禁止坐标降级;新页面必须先取得版本化真机 + 证据并在独立任务中定义识别条件。 +- 候选 session 的候选尝试与滚动预算在动作发出前扣减,retry 不得重置;详情动作 + 白名单只有截图和返回。 +- 截图发送给模型前裁剪无关区域并按配置脱敏。 +- 每个动作记录抽象步骤,不默认记录完整输入文本。 +- 发现验证码、风险控制、支付、生物识别或系统权限页面立即停止。 +- T-217 及更早流程用代码级 allowlist 禁止“提交订单/支付”;T-218 只在服务端围栏和 + `CLICK_ASSUMED` 持久化后开放一次精确“提交订单”,支付动作始终禁止,不能仅靠 + 提示词约束模型。 +- T-246 登录后使用单协程定时 heartbeat/claim/start;本机 VLM 配置完整时自动走 + `AI_ASSISTED`,否则走确定性 `MANUAL_FIRST`。repository mutex 和既有幂等键保持 + 单 claim、单 execution、单探针。 +- 候选商品身份是 `candidate_key + PINDUODUO goods_id + canonical URL`。语义读取和 + 本地 OCR 只能提取唯一 ID,不得推测;后端不访问 PDD URL。 +- Admin 授权快照和 schema v2 设备命令都绑定商品 ID/URL 并纳入命令哈希。Roubao 使用 + 限定拼多多包名的 `ACTION_VIEW` 定向打开,复核商品 ID/标题后才允许选择规格和数量。 +- 后台已请求取消且租约过期的执行中任务由设备 heartbeat 事务原子转为 `CANCELED`, + 同时结束 execution、释放设备并写 `TASK_CANCELED` 审计事件;不会重新排队执行。 + +## 八、错误分类 + +| 错误码前缀 | 示例 | 处理 | +| --- | --- | --- | +| `DEVICE_` | 权限缺失、拼多多未安装 | 修复设备后再领取 | +| `AUTH_` | 登录失效、设备禁用 | 停止并重新认证 | +| `TASK_` | 租约失效、状态冲突 | 刷新服务端状态 | +| `AI_` | 超时、输出无效、低置信度 | 有界重试或转人工 | +| `PDD_` | 未知页面、验证码、风控 | 立即停止,不绕过 | +| `NETWORK_` | API 超时、上传失败 | 保留本地证据,幂等重试 | +| `SAFETY_` | 即将进入禁止动作 | 阻断并记录高优先级事件 | + +## 九、开发顺序 + +1. 接入 Android 基线并记录真实构建方式。 +2. 导入一条脱敏蝦皮样本,以固定搜索词跑通拼多多页面探针。 +3. 接入需求提取和候选判断,验证结构化输出。 +4. 建立后端数据模型、API 和简单管理 Web。 +5. 接入 App 领取、租约、进度和结果。 +6. 20 条真实任务试验;根据数据决定是否进入 V2。 + +## 十、关键风险 + +| 风险 | 影响 | 应对 | +| --- | --- | --- | +| 拼多多 UI/风控变化 | 自动化失败或账号受限 | 有界步骤、版本基线、未知即停、人工接管 | +| VLM 误判 | 候选不匹配或违反预算 | schema + 硬约束复核 + 人工确认 | +| Android 后台限制 | 任务中断 | 前台服务、心跳、恢复状态机 | +| 网络重复请求 | 重复领取或重复结果 | 幂等键、版本号、事务和状态查询 | +| 图片/截图隐私 | 数据泄露 | 鉴权存储、脱敏、保留期限、密钥后端化 | +| 需求膨胀 | 跳过核心可行性验证 | 严格按 Phase 和 P0 范围执行 |