Files
cmroubao/docs/04-architecture.md
T

429 lines
18 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 / 文件存储 +--> VLM Provider
^
|
Android 采购 App
|
+--> AccessibilityService
|
v
拼多多 App
```
- 管理 Web 与 API 同属 `backend-api/`,但页面层不能直接访问数据库。
- Android App 位于 `android-buyer/`,通过 API 领取任务、调用 AI 能力和回传证据。
- 拼多多是第三方受控边界,只能由 Android 设备在已登录会话中操作。
- VLM 密钥的正式路径保留在后端;技术探针如需端上临时调用,只能使用本地忽略配置。
## 二、模块职责
### 2.1 Backend API
建议分层:
```text
cmd/api/ # 进程入口、依赖装配、信号和优雅关闭
internal/transport/http/ # Gin 路由、中间件、页面和请求/响应 DTO
internal/usecase/ # 创建、领取、状态迁移和结果归档
internal/domain/ # 实体、状态机、权限和确定性约束
internal/repository/sqlite/ # database/sql 仓储实现
internal/platform/ # 配置、日志、文件存储和 VLM 适配器
```
职责:
- 验证管理会话、采购人员和设备身份。
- 创建任务并保存原始输入,不让模型结果覆盖原始事实。
- 原子领取下一条任务,签发有期限的 claim lease。
- 校验任务状态迁移、幂等键和设备归属。
- 代理 VLM 调用并把供应商响应转换为领域结构。
- 保存候选、事件、截图元数据和结果。
- 为管理 Web 提供任务列表、详情和取消能力。
不得:
- 根据 Web 按钮直接驱动某台手机点击。
- 在 Gin handler 中写任务状态机、SQL 或仓储业务逻辑。
- 保存拼多多密码、支付凭证或支付验证码。
### 2.2 Android App
建议模块:
```text
ui/ # 登录、任务列表、执行状态、设置
taskclient/ # 后端 API、认证、幂等和重试
tasksource/ # 统一任务源;本地探针与后端 API 实现
workflow/ # ProcurementWorkflow 状态机
automation/ # Accessibility 节点、截图、点击、输入、滑动
ai/ # 结构化 AI 请求/响应,不含供应商业务类型
evidence/ # 截图、步骤日志、脱敏和上传
```
职责:
- 检查无障碍、拼多多安装/登录可用性、网络和当前任务状态。
- 由用户点击后领取任务;一台设备一次只运行一条。
- 执行有界步骤:解析 -> 搜索 -> 浏览不超过 5 个候选 -> 判断 -> 停止确认。
- 显示当前步骤、停止原因和人工接管入口。
- 用前台服务承载执行中任务,进程重启后从服务端状态恢复或安全失败。
- 回传步骤事件、候选、截图和最终结果。
不得:
- 接收到通知就无条件开始采购。
- 只用固定屏幕坐标定位关键控件。
- 在验证码、登录、风险提示或未知页面上继续猜测操作。
- 在 MVP 点击最终“提交订单”或支付相关控件。
### 2.3 VLM Gateway
只负责两类能力:
1. **需求提取**:参考图 + 标题 + SKU -> 搜索词、类目、属性、置信度、警告。
2. **候选评估**:候选截图/文本 + 原始约束 -> 匹配项、缺失项、拒绝原因、建议分。
模型输出是不可信建议,必须通过 schema 和确定性校验:
- `sku`、`quantity` 和 `max_budget` 使用原始任务值;数量和预算不进入模型 prompt。
- 价格未知、超预算或关键属性无法确认时不能判为可接受。
- T-103 探针阈值为 `0.75`;低于阈值或标题/图片冲突时进入人工处理,不自动扩大
浏览范围。
- 模型不能返回点击坐标、状态迁移或“允许提交订单”等执行授权。
T-104 按候选 ordinal 串行评估,每个候选最多一次结构化调用、整批最多 5 次;首个
网络失败或无效输出停止后续调用。模型响应只允许
`schema_version/candidate_index/decision/score/matched/missing_or_uncertain/
rejection_reasons/confidence`,其中 `decision` 仅允许 `REVIEW/REJECT/
MANUAL_REQUIRED`。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`。
技术探针每次点击最多发出一次 VLM 请求,不沿用通用 Agent 的重试循环。该结构化
HTTP 客户端关闭连接自动重试、HTTP/HTTPS 重定向,整体调用超时为 75 秒;协程取消
会取消底层 HTTP call,成功响应正文上限为 64 KiB,非成功响应不读取正文。
远程 provider 只允许 HTTPS;HTTP 仅允许无 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 不写普通日志;加密密钥存储不可用且存在
密钥时拒绝调用。
## 三、核心数据流
### 3.1 技术探针
```text
本机私有蝦皮订单目录
-> Debug 导入工具按订单号配对文本和参考图
-> 规范化为 ProbeTask
-> Android FixtureTaskSource
-> 校验设备就绪
-> 解析为固定/模型搜索词
-> 打开拼多多并搜索
-> 检查最多 5 个候选
-> 到达候选详情或确认页
-> 停止并记录结论
```
该阶段不依赖后台,用于尽早判断 Android 自动化是否可行。
`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.png + manifest.json
-> 一次全局返回并复核固定词结果页
```
候选卡只保留语义指纹和计数,详情截图保存在 App 内部 cache。manifest 记录匿名文件名、
截图 SHA-256、字节数和尺寸,不保存商品标题或页面原文。T-104 使用这些证据时必须通过
受控 evidence 边界读取,不能让 VLM adapter 自行遍历 cache。Android 10/API 29 及
以下不能运行当前截图探针,应在预检时明确不支持,不使用媒体投影或 shell 绕过。
`CandidateEvidenceSource` 只接受当前 workflow 内存中的连续 ordinal 元数据,文件名
固定为 `candidate-01.png` 至 `candidate-05.png`;每张 PNG 最多 8 MiB、全批最多
32 MiB、声明尺寸最长边不超过 10000 px,并复核规范路径、PNG/IHDR、精确字节数、
尺寸和 SHA-256。Android gateway 再解码并把最长边缩至 2048 px。当前需求快照与
搜索词必须同时匹配候选 session,否则拒绝评估;另一个关键词的结果页只允许重新进入
搜索框,不能采集候选。
评估结束后 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
-> 定期 heartbeat 续租并上报步骤
-> 需求提取和拼多多自动化
-> WAITING_CONFIRMATION
-> 采购员接受/拒绝候选
-> 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 分钟,运行时每 30 秒心跳续租;数值进入配置。
- 只有当前设备和有效 claim token 能启动、续租、上报或结束任务。
- 终态不可回退;重试创建新的 execution attempt,不篡改历史证据。
- 取消正在自动化的任务时,App 应在下一个安全检查点停止。
### 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 版本 |
| `pdd_version` | nullable | 已验证拼多多版本 |
| `last_seen_at` | nullable | 最近心跳 |
| `is_enabled` | NOT NULL | 后端开关 |
### `purchase_tasks`
| 字段 | 约束 | 说明 |
| --- | --- | --- |
| `id` | PK | 任务 ID |
| `title` | NOT NULL | 原始标题 |
| `description` | NOT NULL | 原始说明,可为空字符串 |
| `image_asset_id` | FK, NOT NULL | 原始参考图 |
| `quantity` | `> 0` | 权威数量 |
| `max_budget` | `> 0`, nullable | 权威最高预算,币种为 CNY |
| `status` | NOT NULL | 任务状态枚举 |
| `created_by` | FK | 创建人 |
| `claimed_by_device_id` | FK, nullable | 当前设备 |
| `claim_expires_at` | nullable | 租约到期时间 |
| `version` | NOT NULL | 乐观锁/状态并发控制 |
| `created_at/updated_at` | NOT NULL | 审计时间 |
### `task_executions`
| 字段 | 约束 | 说明 |
| --- | --- | --- |
| `id` | PK | 一次执行尝试 |
| `task_id` | FK | 所属任务 |
| `attempt_no` | UNIQUE(task, no) | 尝试序号 |
| `device_id/user_id` | FK | 执行设备和人员 |
| `extracted_requirements` | JSON | 已校验的模型结果 |
| `candidate_result` | JSON, nullable | 候选及匹配理由 |
| `outcome` | nullable | `CANDIDATE_ACCEPTED`、`CANDIDATE_REJECTED`、`NO_MATCH` 或 `MANUAL_REQUIRED` |
| `order_submitted` | NOT NULL, false | MVP 数据库约束必须为 false |
| `error_code/error_message` | nullable | 结构化失败 |
| `started_at/finished_at` | nullable | 执行耗时 |
### `execution_events` 与 `assets`
- `execution_events` 只追加,记录 step、事件类型、可读消息和时间;不保存密码或 token。
- `assets` 保存文件相对路径、媒体类型、大小、哈希、创建者和保留时间。
- 原图和截图必须通过鉴权接口读取,文件名不能直接作为公开 URL。
## 六、API 和并发边界
- 合约以 [`api.md`](api.md) 为准。
- `claim-next` 必须由 usecase 调用 repository,在一个数据库事务内完成
“选取 + 校验设备空闲 + 更新状态”。
- 创建、领取和完成接口支持 `Idempotency-Key`。
- App 对网络超时不能假定失败;必须用任务详情确认服务端最终状态。
- 后端拒绝非法状态迁移,即使客户端 UI 隐藏了对应按钮。
- 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 范围执行 |