Files
cmroubao/docs/04-architecture.md
T

615 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 架构设计
## 一、系统结构
目标架构是一套后端服务、两个面向不同角色的客户端:
```text
采购管理员
|
v
管理 Web(服务端渲染)
|
v
统一 Backend API
|
+--> SQLite / 文件存储
^
|
Android 采购 App --------------------> VLM Provider
|
+--> AccessibilityService
|
v
拼多多 App
```
- 管理 Web 与 API 同属 `backend-api/`,但页面层不能直接访问数据库。
- Android App 位于 `android-buyer/`,通过 API 领取任务和回传证据,在本地调用 AI。
- 拼多多是第三方受控边界,只能由 Android 设备在已登录会话中操作。
- VLM 配置和 Key 位于手机,Key 使用 Android Keystore 包装的加密存储;管理后端不
保存、下发或代理模型调用。
## 二、模块职责
### 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. **候选评估**:候选详情截图/文本 + 原始约束 -> 颜色/尺码硬约束结果、匹配项、
缺失项、拒绝原因、建议分。
正式数据流固定为 `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 开始时固定并写入结果;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 时拒绝调用。
## 三、核心数据流
### 3.1 技术探针
```text
本机私有蝦皮订单目录
-> Debug 导入工具按订单号配对文本和参考图
-> 规范化为 ProbeTask
-> Android FixtureTaskSource
-> 校验设备就绪
-> 校验参考图并提取 SKU 颜色/尺码硬约束
-> 将一次执行专用参考图写入受控 MediaStore
-> 通过拼多多可见图片搜索入口选择该图
-> 检查图片结果页最多 5 个候选
-> 过滤并排序颜色/尺码均确认匹配的 0..5 个候选
-> 到达候选详情或确认页
-> 停止并记录结论
```
该阶段不依赖后台,用于尽早判断 Android 自动化是否可行。
图片搜索临时资产只保存匿名执行标识,不使用订单号或标题作为文件名。App 在写入前
复核任务参考图的媒体类型、长度和 SHA-256,并在完成、失败或安全停止后 best-effort
删除自己创建的媒体。拼多多图片入口、相册页、当前图片或结果页无法唯一确认时停止;
不得从用户相册中猜选其他图片。图片搜索只负责召回,VLM 不能改变原始 SKU,也不能
把 `UNKNOWN` 规格提升为通过。
`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 个去重商品卡
-> 验证详情页
-> 详情页无障碍截图
-> 唯一、安全、非交易规格入口
-> 规格弹层只读语义和截图
-> App cache/candidate-NN-{detail,specification}.png + manifest.json
-> 关闭规格弹层并复核详情页
-> 一次全局返回并复核固定词结果页
```
候选卡只保留语义指纹和计数,详情/规格截图保存在 App 内部 cache。manifest v2 记录
匿名文件名、截图 SHA-256、字节数、尺寸和有界规格语义,不保存商品标题或完整页面
原文。T-104/T-213 使用这些证据时必须通过受控 evidence 边界读取,不能让 VLM adapter
自行遍历 cache。Android 10/API 29 及以下不能运行当前截图探针,应在预检时明确
不支持,不使用媒体投影或 shell 绕过。
`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 规格核验只允许精确语义为“选择规格/请选择规格/颜色分类/颜色款式/选择颜色/
选择尺码”或“已选/请选择”前缀的唯一节点及其可点击祖先。弹层内不点击任何规格选项,
只读取最多 160 个语义节点、每组最多 30 个选项及 selected/enabled/scrollable 语义。
颜色和尺码各自必须是唯一分组:目标值明确且可用为 `MATCH`,完整分组明确缺失或目标
禁用为 `MISMATCH`,重复、缺组、滚动截断或无障碍语义不足为 `UNKNOWN`。
候选评估 schema/prompt v3 把本地规格结果作为权威输入;模型返回的颜色/尺码状态必须
逐项与本地结果相同,否则整批降级人工检查。详情和规格证据 SHA-256 同时绑定
assessment、重排身份和 outbox 资产,VLM 不能通过主图推断覆盖本地硬约束。
PKG110 上的拼多多 8.17.0 服装详情只暴露不可点击的“颜色/款式”预览;完整颜色/尺码
弹层只能通过“单独购买/免拼购买”控件进入。自动化不得点击这两个交易语义控件,也
不得用坐标降级,因此当前版本在没有独立可点击规格入口时返回安全阻塞。是否把购买
语义控件重新定义为只读弹层入口,必须单独做产品/安全决策后变更本边界。
评估结束后 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
-> 管理端展示结果
```
## 四、状态机
### 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 固定 8 小时绝对有效;App access token 固定 1 小时。
- 每次鉴权都联查用户/设备启用状态,因此禁用立即生效,不等待 token 到期。
- 管理 Cookie 与 App Bearer token 使用不同 middleware,不能互相替代。
- T-203 的 `local-admin` 继续作为单管理共享资源 scope;真实 `user_id` 另作 actor,
避免切换身份后隐藏已有任务。
- 管理和 App 登录在 bcrypt 前共享一个内存有界限流器,但使用独立 scope + 服务端
`RemoteAddr` 键;成功登录清零,超限返回 `429` 与 `Retry-After`。
### `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-207 先完成候选、事件、截图和最小结果回传。T-208 在 `task_executions` 下增加
独立、不可变的数据层,不把整批候选塞进 `task_events`、`purchase_tasks` JSON 或
一段不可查询的自由文本:
| 表 | 主要内容 |
| --- | --- |
| `candidate_search_runs` | execution、需求快照 hash、搜索词、App/拼多多版本、开始/结束时间 |
| `candidate_observations` | run、ordinal、可选平台商品 ID/规范化 URL、可见标题/规格/价格、采集状态和截图 asset |
| `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 校验、脱敏和受鉴权访问的时间点快照。
人工理由使用版本化 allowlist。接受或拒绝至少有一个理由且指定主要理由;
`OTHER` 才要求 4-200 字备注。拒绝推荐后改选必须同时产生一条原推荐项负标签和一条
替代项正标签;全部无匹配时每个曝光候选都有负标签。人工修正追加新 review 并引用
旧 review,不能覆盖历史。模型理由可以展示给人员参考,但不能自动成为人工理由。
## 六、API 和并发边界
- 合约以 [`api.md`](api.md) 为准。
- `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 不得重置;详情动作
白名单只有截图和返回。
- 截图发送给模型前裁剪无关区域并按配置脱敏。
- 每个动作记录抽象步骤,不默认记录完整输入文本。
- 发现验证码、风险控制、支付、生物识别或系统权限页面立即停止。
- MVP 用代码级 allowlist 禁止所有已知“提交订单/支付”动作;不能仅靠提示词约束模型。
## 八、错误分类
| 错误码前缀 | 示例 | 处理 |
| --- | --- | --- |
| `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 范围执行 |