diff --git a/routes.md b/routes.md deleted file mode 100644 index 5f63c63..0000000 --- a/routes.md +++ /dev/null @@ -1,99 +0,0 @@ - -> 同步来源:[`docs/routes.md`](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/docs/routes.md) · commit `afc651f75a3a` - -# 路由与页面结构 - -T-202 的离线 P0 页面入口见[原型索引](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/docs/design/index.html)。原型仅用于确认页面区块、 -控件、文案和主要动作,不代表正式路由实现。 - -## 管理 Web - -| 路由 | 页面 | 职责 | 用户故事 | 交互 | -| --- | --- | --- | --- | --- | -| `/login` | 管理登录 | 建立服务端管理会话 | US-007 | IX-001 | -| `/tasks` | 任务列表 | 查看状态、筛选并进入详情 | US-002 | IX-003 | -| `/tasks/new` | 新建任务 | 提交图片和采购约束 | US-001 | IX-002 | -| `/tasks/{id}` | 任务详情 | 查看输入/候选/证据、创建下单授权,并展示对账中/人工对账/待人工付款状态 | US-002、US-006、US-010 | IX-003、IX-008、IX-011 | -| `/freight` | ERP 货运列表 | 查看已导入货运单并进入全部商品明细 | US-011 | IX-012 | -| `/freight/import` | ERP 货运导入 | 创建精确单号、最多 7 天日期范围或“同步至现在”任务,并查看成功水位 | US-011 | IX-012 | -| `/freight/{id}` | ERP 货运详情 | 查看全部商品明细、来源变化、补图和采购任务生成状态 | US-011、US-012 | IX-012、IX-013 | - -MVP 登录后默认进入 `/tasks`。未登录访问受保护页面时跳转 `/login` 并携带安全的 -站内返回路径;只接受 `/tasks`、`/freight` 及其本站子路径,拒绝绝对 URL、`//` -和反斜杠。 -不存在和无权限必须使用不同内部原因,但页面均不得泄露任务内容。 - -## Android 页面 - -Android 导航名称是逻辑目的地,具体 Compose/Fragment 形式待接入 Roubao 源码后确定。 - -| 目的地 | 页面 | 职责 | 用户故事 | 交互 | -| --- | --- | --- | --- | --- | -| `login` | 登录/设备绑定 | 建立人员与设备身份 | US-007 | IX-004 | -| `tasks` | 任务主页 | 就绪检查、获取或继续任务 | US-003 | IX-005 | -| `task/{id}/preview` | 任务预览 | 检查原始要求并开始 | US-003、US-004 | IX-005、IX-006 | -| `task/{id}/running` | 任务执行 | 显示步骤、返回拼多多、安全停止 | US-004、US-006 | IX-006、IX-008 | -| `task/{id}/confirm` | 候选确认 | 接受、拒绝、无匹配或转人工;T-208 增加逐候选结构化理由 | US-005、US-008 | IX-007、IX-009 | -| `task/{id}/result` | 任务结果 | 查看终态和同步状态 | US-002、US-006 | IX-007、IX-008 | -| `settings` | 设备设置 | 查看权限、后端、执行模式和本地 VLM 配置 | US-003、US-007、US-009 | IX-004、IX-005、IX-010 | - -## App 后端路由 - -以下路由只接受有效 BUYER Bearer token,认证 principal 中的用户和设备是权威身份: - -| 路由 | 职责 | -| --- | --- | -| `POST /api/v1/devices/heartbeat` | 上报版本、就绪位并核对服务端活跃任务 | -| `POST /api/v1/tasks/claim-next` | 用户点击后原子领取或重放领取结果 | -| `GET /api/v1/tasks/{id}/reference-image` | 用当前 claim 读取该任务匿名参考图 | -| `POST /api/v1/tasks/{id}/start` | `CLAIMED -> RUNNING` 并创建 execution | -| `POST /api/v1/tasks/{id}/heartbeat` | 更新 step、设备在线时间和运行租约 | -| `POST /api/v1/tasks/{id}/release` | 未开始时 `CLAIMED -> PENDING` | -| `POST /api/v1/tasks/{id}/cancel-ack` | 安全停止后确认 `CANCELED` 并结束 execution | -| `POST /api/v1/tasks/{id}/events` | 幂等补报本地执行事件 | -| `POST /api/v1/tasks/{id}/candidates` | 保存最多 5 个候选、模型判断和本地推荐 | -| `POST /api/v1/tasks/{id}/commands/next` | 拉取或重放 Admin 已创建的同一条下单命令 | -| `POST /api/v1/tasks/{id}/commands/{command_id}/ack` | App 加密落盘后幂等确认命令 | -| `POST /api/v1/tasks/{id}/order-dry-runs/start` | 加密保存执行意图后开始已授权商品重新定位 | -| `POST /api/v1/tasks/{id}/order-dry-runs/{command_id}/ready` | 回传确认订单页前的 SKU/数量/金额与证据 | -| `POST /api/v1/tasks/{id}/complete` | 提交人工结果,强制 `order_submitted=false` | -| `POST /api/v1/tasks/{id}/fail` | 提交结构化失败和受控证据 | - -claim/start/release/cancel-ack 使用相应幂等规则;参考图、start、task heartbeat、 -release 和 cancel-ack 都必须匹配当前 `X-Claim-Token` 与 `claim_generation`。 -运行授权过期后只有 task heartbeat 状态同步和已请求取消的 cancel-ack 允许原设备 -继续调用;它们不赋予 App 恢复外部动作的权限。 -T-207 的 events/candidates/complete/fail 接受原设备对授权内已产生结果的离线补报, -并由服务端记录是否在执行授权过期后收到;后端不提供 VLM 代理路由。 - -## 导航规则 - -- App 有活跃任务时启动后优先恢复该任务,不允许直接领取下一条。 -- `CLAIMED` 进入预览;`RUNNING` 进入执行;`WAITING_CONFIRMATION` 进入等待 Admin; - 终态进入结果。 -- 从执行页切换拼多多后,使用前台服务通知返回执行页。 -- 候选确认页返回不能恢复自动化。 -- 登录过期时先保存非敏感本地状态,再跳转登录;重新登录后查询服务端状态。 -- 设备设置不是首屏,任务主页是采购人员的工作入口。 -- 后台任务不能携带或修改 provider、模型、Base URL 和 API Key;App 设置由设备 - 操作者管理,Key 使用 Keystore-backed 加密存储。 -- 管理后端离线时执行页显示授权截止时间和待同步数量;到期后只能安全停止和等待 - 联网,不能领取或继续页面动作。 - -## 页面组件边界 - -| 组件 | 归属 | 说明 | -| --- | --- | --- | -| `TaskForm` | 管理 Web | 只管理表单状态和可访问反馈,创建逻辑在 usecase | -| `TaskStatusBadge` | 管理 Web | 统一状态中文和语义,不内置状态迁移 | -| `ExecutionTimeline` | 管理 Web/App | 展示只追加事件 | -| `ReadinessPanel` | Android | 汇总权限、安装、网络和活跃任务 | -| `TaskRequirementSummary` | Android/Web | 展示原始输入和 AI 派生信息,视觉上明确区分 | -| `CandidateReview` | Android/Web | 展示候选与硬约束校验,不执行自动化动作 | -| `ExecutionController` | Android | UI 命令入口,委托 workflow,不直接调用 Accessibility | -| `PendingPaymentSummary` | 管理 Web/App | 只读展示对账订单与人工付款提醒,不提供付款或重提动作 | -| `FreightSyncForm` | 管理 Web | 创建精确单号/日期同步记录、查看成功水位,不持有 ERP 凭证 | -| `FreightItemReview` | 管理 Web | 显示规范化来源字段、缺失项和采购任务生成操作 | - -具体状态反馈以[交互清单](/chengma/mroubao/wiki/08-interaction-checklist)为准,组件命名可在接入真实框架后 -调整并同步本文。 diff --git a/unnamed.md b/unnamed.md index 03c961b..6c9bb5e 100644 --- a/unnamed.md +++ b/unnamed.md @@ -1,1066 +1,99 @@ - -> 同步来源:[`docs/api.md`](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/docs/api.md) · commit `afc651f75a3a` - -# API 合约 - -> MVP 使用 `/api/v1`、UTF-8 JSON over HTTPS。文件上传使用 `multipart/form-data`。 -> Web 页面可以直接调用同一 usecase,但不能形成不同的业务规则。 - -## 通用约定 - -- 所有 ID 为 UUID 字符串。 -- 时间为 UTC ISO 8601,例如 `2026-07-25T08:30:00Z`。 -- 金额使用十进制定点字符串,例如 `"199.00"`,币种固定 `CNY`;不使用浮点数。 -- 写接口在合约标注处接收 `Idempotency-Key` 请求头。 -- App 的任务执行接口同时需要用户 Bearer Token、设备身份和 `X-Claim-Token`。 -- 分页使用 `limit` 和不透明 `cursor`;MVP `limit` 最大 100。 -- 客户端不得根据 HTTP 超时判断操作失败,必须查询资源最终状态。 - -管理 Web/API 使用 ADMIN 服务端会话;App 执行接口使用 BUYER + 设备 Bearer token。 -两种身份不能互换。HTTP 明文只允许 loopback 开发监听,非 loopback 服务必须配置 -certificate/private key 并直接启用 TLS。 - -通用错误: - -```json -{ - "error": { - "code": "TASK_STATE_CONFLICT", - "message": "任务当前状态不允许此操作", - "retryable": false, - "details": {} - }, - "request_id": "f2cf02e9-57e0-4fca-817b-c85228acfc81" -} -``` - -HTTP 语义: - -- `400` 请求格式或业务校验失败 -- `401` 未建立有效身份 -- `403` 身份有效但无权限、设备禁用或 claim 不属于当前设备 -- `404` 资源不存在,外部响应不泄露其他人的资源存在性 -- `409` 幂等冲突、状态冲突或设备已有活跃任务 -- `413` 文件过大 -- `415` 不支持的媒体类型 -- `422` 结构可解析但字段不满足 schema -- `429` 频率限制 -- `503` 依赖暂不可用 - -## 服务健康 - -### `GET /healthz` - -不需要业务身份,只检查进程和 SQLite 连接,不返回版本、路径、DSN、连接池统计或内部 -错误。响应带 `Cache-Control: no-store`,默认不返回 CORS header。 - -数据库可用: - -```json -{"status":"ok"} -``` - -返回 `200`。数据库不可用时返回 `503`: - -```json -{"status":"unavailable"} -``` - -健康检查失败不能终止进程;未知路由和不允许的方法分别使用稳定 `404`/`405` JSON。 - -## 认证 - -### 管理 Web 会话 - -`POST /login` 接受表单账号密码,成功后设置 `HttpOnly`、`Secure`、`SameSite=Lax` -会话 Cookie。loopback HTTP 开发时不设置 `Secure`,非 loopback 服务必须直接启用 -TLS。管理会话固定30天绝对有效期,不滚动续期;既有会话不静默延期,部署后需重新登录 -才取得新期限。`POST /logout` 撤销服务端会话并清除 Cookie。Web 会话不能调用设备执行 -接口。 - -未登录页面请求以 `303` 跳转 `/login?next=...`;`next` 只允许 `/tasks` 及其本站 -子路径。未授权管理 API 返回 `401 ADMIN_SESSION_REQUIRED`。Cookie 认证的管理 API -写请求除原 Content-Type/幂等要求外,还必须携带与 CSRF Cookie 匹配的 -`X-CSRF-Token`。 - -`POST /login` 在通过表单和 CSRF 校验、执行 bcrypt 前,按服务端观察到的来源地址 -限流。5 分钟内最多 10 次;成功登录清零。超限返回 `429`、`Retry-After` 和通用 -中文提示,不暴露账号是否存在。 - -### `POST /api/v1/auth/token` - -采购 App 建立用户和设备联合会话。 - -```json -{ - "username": "buyer01", - "password": "local-input-only", - "device_id": "f0c3d438-1c44-4f80-b898-c645afe7eaa8", - "device_token": "local-input-only", - "app_version": "0.1.0", - "android_version": "待设备填写" -} -``` - -成功: - -```json -{ - "access_token": "opaque-token-returned-once", - "token_type": "Bearer", - "expires_in": 2592000, - "user": { - "id": "d950db90-cf58-4f21-86fd-067e1a91a3a6", - "username": "buyer01", - "role": "BUYER" - }, - "device": { - "id": "f0c3d438-1c44-4f80-b898-c645afe7eaa8", - "enabled": true - } -} -``` - -密码和设备 token 不得出现在响应、日志或 execution event 中。 - -T-204 固定使用 256 bit 随机 opaque access token,数据库只保存 SHA-256;T-266 -将新签发 token 的绝对有效期调整为固定30天(`2592000` 秒),不滚动续期。既有 token -保留原 `expires_at`,部署后重新登录才取得30天期限。用户必须为有效 `BUYER`,设备 -必须预授权且启用;未绑定设备在首次成功登录时原子绑定当前采购员,已绑定其他采购员 -时拒绝。不提供设备自助登记、refresh 或 App logout;Android 安全存储接入属于 T-206。 - -App token 登录与管理登录使用独立限流 scope,同样为每个来源地址 5 分钟最多 10 次。 -超限返回 `429`、`Retry-After` 和稳定错误码 `AUTH_RATE_LIMITED`,`retryable=true`。 - -## 资产 - -### `POST /api/v1/assets` - -管理会话上传任务参考图片;App 也可用设备会话上传执行截图。请求必须带 -`Idempotency-Key`。 - -表单字段: - -- `file`:必填,MVP 允许可解码 JPEG/PNG/WebP;请求最多 20 MiB、最长边最多 - 10000 px、总像素最多 25 MP。 -- `purpose`:`TASK_REFERENCE` 或 `EXECUTION_EVIDENCE`。 -- `task_id`:证据图片必填;参考图创建时为空。 - -成功: - -```json -{ - "id": "2bbc1bf2-30f7-497e-a52b-bcb4c61d57c3", - "media_type": "image/jpeg", - "size_bytes": 183245, - "sha256": "hex-value", - "created_at": "2026-07-25T08:30:00Z" -} -``` - -T-203 成功返回 `201`。使用相同 `Idempotency-Key` 和相同图片内容重试时返回同一 -资产;同 key 不同内容返回 `409`。 - -`TASK_REFERENCE` 在后端统一白底合成、缩放到最长边不超过 2048 px,并以质量 90 -编码为匿名 JPEG。响应的 `media_type`、`size_bytes` 和 `sha256` 都描述规范化结果, -不描述原始上传文件。T-203 尚不接受 `EXECUTION_EVIDENCE`。 - -### `GET /api/v1/assets/{asset_id}/content` - -受鉴权的文件流。只能读取用户有权查看的任务资产;不返回服务端文件路径。 - -## Go ERP 会话与货运信息 - -顺运宝仅由 Go API 进程直连;浏览器不复用 ERP 凭证,也不接收 Cookie、JWT、用户资料或 -原始响应。货运同步只接收以下最小化规范结构: - -```json -{ - "schema_version": 1, - "query": {"mode":"ORDER_NUMBER"}, - "orders": [{ - "external_stock_id": "99001122", - "source_code": "masked-at-log-boundary", - "platform_order_no": "optional", - "shop_name": "来源店铺", - "source_created_at": "2026-07-28T08:00:00+08:00", - "order_status": "0", - "purchase_status": "0", - "is_canceled": false, - "items": [{ - "external_item_id": "880011", - "title": "商品标题", - "product_spec": "灰色,2XL", - "sku": "灰色,2XL", - "quantity": 2, - "product_thumb_ref": "190000000", - "original_unit_price_minor": 12950, - "original_currency": "TWD", - "purchase_status": "0" - }] - }] -} -``` - -不得返回 receiver、receiverTel、receiverAddr、Cookie、JWT、ERP 用户资料或完整原始 -对象。多商品必须全部保留;缺失详情返回协议错误,不允许部分成功。 - -商品 `product_spec` 和采购使用的 `sku` 均来自 ERP `productSpec`,不得静默回退 -`sku/variationSku`。`productPrice` 严格转换为 `original_unit_price_minor`;缺失价格为 -`null`,币种固定为 `TWD`。`product_thumb_ref` 来自商品 `productThumb`,存在时必须是 -规范化正整数,不能使用货运单 ID、商品明细 ID 或完整 URL。 - -货运详情中的每个商品还返回: - -```json -{ - "image_status": "READY", - "image_error_code": null, - "image_url": "/api/v1/freight-items/{item_id}/image" -} -``` - -`image_status` 为 `NONE/PENDING/READY/MISSING/FAILED`。只有 `READY` 返回同源 -`image_url`;其他状态返回 `null`,失败时 `image_error_code` 是不包含上游正文的稳定错误码。 -元数据同步先提交,图片在剩余请求预算内 best-effort 缓存,因此图片失败不改变货运同步的 -`SUCCEEDED` 终态。后续新同步会重试非 `READY` 图片,相同引用的 `READY` 图片不会重复下载。 - -### `GET /api/v1/freight-items/{item_id}/image` - -Admin 会话鉴权的本地货运商品图片流。仅当商品仍在当前货运版本中、当前 `productThumb` -与已缓存引用一致且状态为 `READY` 时返回 `200 image/jpeg`;不存在、未完成、引用已变化或 -无权访问统一返回 `404`。响应包含 `Content-Length`、ETag、 -`X-Content-Type-Options: nosniff` 和 `Cache-Control: private, no-store`,不返回 ERP URL、 -查询参数或服务端文件路径。 - -后端只使用配置的 `CMROUBAO_SHUNYUNBAO_URL` origin 和固定 -`/api/p/file?id=` 请求图片,复用 ERP Cookie jar 并禁止重定向。下载内容经过 -既有大小、尺寸、像素和解码校验,统一白底规范化为最长边不超过 2048 px 的 JPEG。 - -### Go ERP 会话(T-230、T-236) - -`/erp`、`/erp/captcha`、`/erp/login` 和 `/api/v1/erp-session*` 不再暴露。创建货运同步 -前,单一 API 进程在未认证时以同一 Cookie jar 获取验证码、调用受控 OCR、登录并校验用户。 -验证码图片、OCR 文字、Cookie、JWT、账号、密码和原始响应都不返回浏览器/API,也不写 SQLite。 -登录成功后从 `data.user` 严格读取 id/username,并请求 -`GET /am/user/get?id=`;返回身份必须一致。只有 HTTP 401/403 或 ERP code `-2` 被视为 -会话失效,其他 `status=false` 返回 `ERP_RESPONSE_INVALID`。 - -ERP 登录明确返回 `status=false`、`msg=图片验证码不正确` 时,首次失败后最多额外重试 6 次, -单次认证最多登录 7 次;每次都重新获取验证码图片并重新调用 OCR。认证总预算为 20 秒,调用方 -deadline 更短时以调用方为准。账号密码错误、其他登录拒绝、OCR 异常、网络错误和协议错误均不 -重试;次数耗尽返回 `ERP_LOGIN_REJECTED`。 - -ERP 配置来源: - -- `CMROUBAO_SHUNYUNBAO_URL`:默认 `https://www.shunyunbaoerp.com`;只接受无路径、 - userinfo、query 或 fragment 的 HTTPS origin。 -- `CMROUBAO_SHUNYUNBAO_USERNAME` 与 `CMROUBAO_SHUNYUNBAO_PASSWORD`:必须同时设置; - 为空时连接页明确显示未配置,服务进程仍可用于其他本地功能。 -- `cmd/api` 只把标准工作目录 `backend-api/.env` 中的上述三个值作为回退。系统环境变量 - 优先;文件缺失不报错。该解析器不修改全局环境,不加载数据库、HTTP、TLS 或 authctl - 密码,也不支持变量展开、命令执行或任意路径。 -- `CMROUBAO_OCR_API_URL` 为可选 OCR `POST` endpoint;只允许 HTTPS,或本机 loopback HTTP, - 不允许 userinfo、query、fragment 或重定向。请求使用 `multipart/form-data` 的 `file` 字段; - OCR 不可达、超时、非成功、过大或响应无有效文本时,货运创建返回 `503 OCR_SERVICE_INVALID` - 且不会创建同步记录。预检的稳定错误还包括 `422 ERP_NOT_CONFIGURED`、 - `422 ERP_LOGIN_REJECTED`、`502 ERP_RESPONSE_INVALID` 和 `503 ERP_UNAVAILABLE`;SSR 导入页 - 用相同 code 显示可关闭弹窗并保留表单。所有 message 都是固定匿名文本,不含 ERP/OCR 原始 - 响应、验证码、Cookie、账号或密码。 -- `CMROUBAO_ERP_DEBUG_LOG` 默认 `false`,仅接受 `true` 或 `false`。临时设为 `true` 后, - API 启动窗口记录 ERP request 的 method/path,以及 response 的 status、content type、长度和 - 最多 4 KiB 的递归脱敏 JSON 摘要;不记录请求 body、完整 URL query、header、Cookie、验证码、 - OCR 文本、账号、密码、token、订单标识、收件信息、非 JSON body 或图片。诊断完成后必须设回 - `false` 并重启 API。Windows 可使用根目录 `start-backend.bat --erp-debug` 仅为本次 API - 进程覆盖开启;也可与 `--migrate` 组合。该模式额外在 ERP 登录前输出单行 - `erp_ocr_result value="..." length=...`,值只限本次有效 OCR 结果;OCR 失败或无效时只输出 - `class=failed` 或 `class=invalid`,不输出原文。每次登录另输出 - `erp_login_attempt attempt= max_attempts=7 result=<固定分类>`,不包含凭证或 ERP 原始响应。 - -会话不写 SQLite 或 Redis;服务重启后在下一次货运导入前重新经 OCR 建立会话。 - -### `POST /api/v1/freight-syncs` - -ADMIN 创建货运同步记录,必须带 `Idempotency-Key`: - -```json -{"mode":"ORDER_NUMBER","order_number":"完整单号"} -``` - -T-224 增加: - -```json -{ - "mode":"CREATED_RANGE", - "created_from":"2026-07-22", - "created_to":"2026-07-28" -} -``` - -手动范围最多 7 个自然日;也可以只提交 -`{"mode":"CREATED_RANGE","sync_to_now":true}`。后者在没有水位时从当天开始,有水位时 -从成功水位前回看 10 分钟对应的自然日开始,后端再按最多 7 天切窗。 - -`ORDER_NUMBER` 在同一请求内执行认证、ERP 查询、规范化和事务落库,总预算 55 秒。首次成功 -返回 `201` 和最终 `SUCCEEDED` run,成功幂等重放返回 `200`,两者均设置 -`Location: /api/v1/freight-orders`。同时只能执行一个完整单号同步,其他请求快速返回 -`409 FREIGHT_SYNC_BUSY`;超时返回 `504 FREIGHT_SYNC_TIMEOUT`,并使用独立 cleanup context -把已创建 run 保存为 `FAILED`。Admin Web 成功后跳转 `/freight?notice=import-succeeded`, -此时列表已能读取货运头和全部商品明细。 - -`CREATED_RANGE` 和 `sync_to_now` 保持异步,响应 `202` 和 `PENDING` run,随后在后台执行。 -订单号不进入 URL、事件 message 或访问日志;数据库只保存规范值及用于审计/检索的受控字段, -不保存 ERP 凭证、Cookie 或 JWT。HTTP Server `WriteTimeout` 为 70 秒,ERP 单次请求 timeout -仍为 30 秒。 - -### `GET /api/v1/freight-syncs/{sync_id}` - -返回 `PENDING/RUNNING/SUCCEEDED/FAILED`、查询模式、匿名查询摘要、开始/结束时间、 -订单/商品计数和稳定错误码。失败不返回 ERP 原始 body 或个人信息。 - -### `GET /api/v1/freight-sync-watermark` - -返回当前 ADMIN creator 的顺运宝最近成功日期同步水位;尚未成功同步时返回 -`{"watermark":null}`。只有完整批次落库成功才在同一事务推进水位;失败、空响应以外 -的协议错误和较旧范围成功均不会覆盖较新的水位。 - -### `GET /api/v1/freight-orders` - -返回当前 ADMIN creator 最近更新的货运单并支持 `limit`(1..100)。响应不包含 -收件人字段或 ERP 原文;列表筛选和 cursor 后置,不属于本次日期同步闭环。 - -### `GET /api/v1/freight-orders/{id}` - -T-222 返回货运头、全部当前商品明细和 revision/hash 状态。T-223 再增加采购需求和 -已生成 task 引用。不存在和跨 creator 统一 404;响应 `Cache-Control: no-store`。 - -T-227 的 Go source 使用当前内存会话,按 `listTotal -> list 分页 -> listByStock` 查询; -完整单号由请求同步调用,日期范围由后台 worker 调用。每次先校验会话;列表最多 100 条、 -每页 20 条,详情每批最多 100 个外部 stock ID。 -顺运宝详情 `created` 已确认可能只有分钟精度;无时区的 -`yyyy-MM-dd HH:mm[:ss]` 或 `yyyy-MM-ddTHH:mm[:ss]` 均严格按 Asia/Shanghai 解释并转 UTC, -RFC3339 保留其显式时区。其他未知时间格式继续返回 `ERP_RESPONSE_INVALID`。 -未配置、未登录、找不到货运单、响应协议错误和暂时不可用分别落为 -`ERP_NOT_CONFIGURED`、`ERP_SESSION_REQUIRED`、`ERP_FREIGHT_NOT_FOUND`、 -`ERP_RESPONSE_INVALID` 和 `ERP_UNAVAILABLE`,不返回 ERP 原始错误 body。 - -### `POST /api/v1/freight-items/{item_id}/procurement-request` - -T-223 创建或重放该商品当前 revision 的采购需求。ERP 数字状态不自动解释,ADMIN -必须显式确认仍需采购: - -```json -{"confirm_procurement_needed":true} -``` - -请求快照保留 title、product spec、SKU、quantity、来源 hash/revision 和当时的取消/ -采购状态。缺标题、SKU、正整数数量或来源明确取消时创建为 `BLOCKED`,不调用 VLM -补字段;字段合格但没有受控参考图时为 `NEEDS_IMAGE`。 - -### `PUT /api/v1/procurement-requests/{id}/reference-asset` - -绑定当前 ADMIN 已上传的 `TASK_REFERENCE` asset。asset 必须尚未属于其他任务/请求, -内容仍走既有解码、像素、规范化和 hash 校验。请求体: - -```json -{"image_asset_id":"2bbc1bf2-30f7-497e-a52b-bcb4c61d57c3"} -``` - -绑定成功后进入 `READY`。来源 revision/hash 或货运取消标记已变化时返回 -`409 PROCUREMENT_SOURCE_CHANGED`。 - -### `POST /api/v1/procurement-requests/{id}/purchase-task` - -必须带 `Idempotency-Key`,请求体是空 JSON 对象 `{}`。只有 `READY` request 可生成 -任务;task、`purchase_task_sources` 和 request 的 `TASK_CREATED` 状态在同一事务内 -提交。同一 request revision 即使使用不同幂等 key 重试也返回同一 task。来源后续 -更新只显示 `source_changed=true`,不修改 PENDING、CLAIMED、RUNNING 或终态任务。 - -管理 Web 在 `/freight/{id}` 内提供对应的人工确认、参考图上传和生成任务操作;写操作 -分别使用 `/freight/items/{id}/procurement-request`、 -`/freight/procurement-requests/{id}/reference` 和 -`/freight/procurement-requests/{id}/purchase-task`,均受 ADMIN session 与 CSRF -保护。 - -## 采购任务 - -### `POST /api/v1/tasks` - -采购管理员或授权的外部管理系统创建任务。必须带 `Idempotency-Key`。 - -```json -{ - "source_ref": "external-admin-task-10001", - "title": "黑色双肩包", - "sku": "BLACK-20L", - "description": "容量约20L,外观接近参考图", - "image_asset_id": "2bbc1bf2-30f7-497e-a52b-bcb4c61d57c3", - "quantity": 2, - "max_budget": "200.00" -} -``` - -规则: - -- `title`、`sku` 和 `image_asset_id` 必填,`description` 可为空。 -- `title` 最多 120 个 Unicode 字符且不超过 2048 个 UTF-8 字节;`sku` 不超过 - 512 个 UTF-8 字节;`description` 不超过 8192 个 UTF-8 字节。 -- `quantity` 是正整数。 -- `max_budget` 可为空,否则为大于零、最多两位小数的 CNY 金额,表示当前任务全部 - 数量的最高商品总预算,不含尚无法确认的运费或优惠。 -- 同一调用方的 `source_ref` 如填写必须唯一。 - -成功返回 `201`: - -```json -{ - "id": "37c9c715-9b51-4ed5-984e-66dad2710c71", - "status": "PENDING", - "title": "黑色双肩包", - "sku": "BLACK-20L", - "quantity": 2, - "max_budget": "200.00", - "created_at": "2026-07-25T08:30:00Z" -} -``` - -### `GET /api/v1/tasks` - -管理端列表。可选参数:`q`、`status`、`created_from`、`created_to`、`limit`、 -`cursor`。`q` 匹配任务 ID、`source_ref`、标题或 SKU;默认 `limit=20`,最大 100。 -排序固定为 `created_at DESC, id DESC`,不透明 cursor 同时编码这两个字段。 - -```json -{ - "items": [ - { - "id": "37c9c715-9b51-4ed5-984e-66dad2710c71", - "title": "黑色双肩包", - "sku": "BLACK-20L", - "status": "RUNNING", - "quantity": 2, - "device_name": "pdd-phone-01", - "updated_at": "2026-07-25T08:35:00Z" - } - ], - "next_cursor": null -} -``` - -### `GET /api/v1/tasks/{task_id}` - -管理会话返回完整任务、当前 execution、时间线、候选和资产元数据。App 只能读取 -当前设备已领取的任务。响应必须同时包含: - -- `original_requirement`:不可变原始输入。 -- `derived_requirement`:模型输出,可能为空。 -- `claim`:管理端可见设备和到期时间;claim token 永不返回。 -- `execution`:step、outcome、错误、`order_submitted`。 -- `events` 和 `assets`:有权限的摘要。 -- `retry`:`allowed`、稳定 `block_code` 和按序保存的重试原因历史。 - -T-204 已实现的 `TASK_CREATED`、`TASK_CANCELED` 事件包含可空 -`actor_user_id`;新管理操作写入真实 ADMIN 用户 ID,T-203 历史事件返回 `null`。 - -### `POST /api/v1/tasks/{task_id}/cancel` - -管理端取消任务。`PENDING` 可立即取消;执行中只设置取消请求,App 在安全检查点确认 -后进入 `CANCELED`。终态返回 `409`。 - -```json -{ - "reason": "需求已撤销" -} -``` - -`PENDING/CLAIMED` 立即进入 `CANCELED`。`RUNNING/WAITING_CONFIRMATION` 只记录 -取消请求并保持原状态;重复请求不重复写事件。App 的任务 heartbeat 会返回 -`cancel_requested=true`,只有 App 在安全检查点调用 `cancel-ack` 后才进入 -`CANCELED` 并结束 execution。 - -### `POST /api/v1/tasks/{task_id}/retry` - -管理端将下单前失败或取消的原采购任务重新排入队列。必须带 `Idempotency-Key`: - -```json -{ - "reason_code": "AUTOMATION_FAILED", - "reason_note": "图片搜索阶段中断" -} -``` - -`reason_code` 允许 `AUTOMATION_FAILED`、`DEVICE_INTERRUPTED`、 -`ADMIN_CANCELED`、`OTHER`;`OTHER` 必须提供不超过 500 UTF-8 字节的 -`reason_note`。 - -仅 `FAILED/CANCELED` 且不存在任何 `order_submissions`、历史 execution outcome -也没有 `order_submitted=true` 时允许。成功复用同一 task ID,将任务改为 -`PENDING` 并增加 version;参考图、ERP 来源、候选、证据和旧 execution 不变,Roubao -再次领取后创建递增的新 execution attempt。响应: - -```json -{ - "task": { - "id": "37c9c715-9b51-4ed5-984e-66dad2710c71", - "status": "PENDING" - }, - "replayed": false -} -``` - -活跃或成功任务返回 `409 TASK_RETRY_NOT_ALLOWED`。只要任务存在提交围栏、待付款订单、 -人工复核或其他下单信号,返回 `409 TASK_RETRY_ORDER_UNCERTAIN`,不提供绕过参数。 -相同 key/body 返回同一任务且 `replayed=true`;同 key 不同 body 返回幂等冲突。 - -### `POST /api/v1/tasks/{task_id}/order-authorizations` - -T-215 起由有效 ADMIN 会话对当前 `WAITING_CONFIRMATION` execution 创建一次性待投递 -授权。Cookie API 请求必须带 `X-CSRF-Token` 和 `Idempotency-Key`;请求体见 -[`T-215`](/chengma/mroubao/wiki/T-215.-)。关键规则: - -- 顶层和每个 item 只引用 v7 `candidate_key`,不得用 ordinal 或 URL 选品。 -- `items` 恰好覆盖当前 execution 全部 observation,只有顶层 key 对应项为 - `ACCEPT`;所有项必须包含 T-208 schema v1 合法理由。 -- `execution_id`、task content hash 和 expected task version 必须同时匹配;服务端 - 从任务/observation 读取 SKU、数量、候选规格、价格和身份指纹写入授权快照。 -- 首次成功返回 `201` 和 `PENDING_DELIVERY` 授权;同 key 同 body 重放返回相同 - 授权并带 `replayed=true`,同 key 不同 body 返回 `409`。 -- 未投递前改选必须引用当前 `supersedes_authorization_id`;旧授权变为 - `SUPERSEDED`。已投递或不是最新授权时返回 `409`。 - -Admin `GET /api/v1/tasks/{task_id}` 同时返回当前及历史 -`order_authorizations`。授权仅允许 T-216 设备命令领取,不代表浏览器可以直接操作 -手机,也不授权付款。 - -### `POST /api/v1/tasks/{task_id}/commands/next` - -T-216 起由原 BUYER/设备主动拉取当前 execution 的下单命令。请求带 bearer、 -`X-Claim-Token`,body 提交 `device_id`、`execution_id` 和 `claim_generation`。 -服务端校验有效 claim/租约、`WAITING_CONFIRMATION`、未取消和同一 active -authorization。没有命令返回 `204`;首次投递把授权置为 `DELIVERED`,之后对 -`DELIVERED/ACKNOWLEDGED` 返回同一 command。历史授权保持 schema v2;T-278 后新授权 -使用 schema v3。 - -命令只含 task/execution/content hash、原始 SKU/数量、candidate key、观测标题/ -规格/价格、T-214 四个指纹、`platform_product_id` 和严格规范化的 -`canonical_product_url`。schema v3 另含冻结的 `authorized_total_price_cap_cents` 和 -`price_cap_source=TASK_MAX_BUDGET|DEFAULT_LOW_PRICE_CAP`;上限来自任务最高总预算, -任务未填价格时来自后台默认低价上限。`observed_ordinal` 仅是审计提示,不能替代商品 ID。 -`command_sha256` 覆盖上述全部字段;投递重试不得变化。新授权只允许具备完整 -`goods_id`/URL 身份的候选,历史无商品身份的授权不能生成自动下单命令。 - -### `POST /api/v1/tasks/{task_id}/commands/{command_id}/ack` - -App 严格校验并写入 Keystore-backed 加密状态后,使用 bearer、claim token、 -`Idempotency-Key` 提交 execution、generation 和 `command_sha256`。成功把 -`DELIVERED -> ACKNOWLEDGED`;相同 key/body 和同 command/hash 可安全重试,其他 -command/hash/设备或无效租约返回冲突。ACK 只表示命令已可靠保存,不表示开始操作、 -创建订单或付款。 - -### `POST /api/v1/tasks/{task_id}/order-dry-runs/start` - -T-217 App 在加密保存 dry-run 意图后调用。请求带 BUYER bearer、claim token、 -`Idempotency-Key`,body 固定 device、execution、generation、command id/hash。 -服务端要求有效租约、任务仍为 `WAITING_CONFIRMATION`、未取消且授权为 -`ACKNOWLEDGED`;事务内创建 PREPARING dry-run、把授权置为 `EXECUTING` 并写开始 -事件。相同请求重放返回同一记录。 - -### `POST /api/v1/tasks/{task_id}/order-dry-runs/{command_id}/ready` - -App 到达确认订单页并先加密保存 READY 后,提交当前 card/detail 指纹、规范化标题、 -已选 SKU、数量、单价、商品总额和受控 evidence asset。服务端重新核对 command、 -task/execution/claim、数量、授权冻结总额上限和 evidence 归属后,把同一 dry-run 置为 READY 并写 -事件;相同请求可重放。该响应不表示订单已提交,且不授权付款。 - -### `POST /api/v1/tasks/{task_id}/order-dry-runs/{command_id}/fail` - -T-279 起,App 在 direct dry-run 无法严格确认商品身份、目标 SKU、当前价格或冻结预算时, -使用 BUYER bearer、claim token 和固定 `Idempotency-Key` 回传失败。body 包含 device、 -execution、generation、`command_sha256`、`failure_code` 和受限失败说明;代码只接受 -`IDENTITY_MISMATCH`、`SKU_UNRESOLVED`、`PRICE_UNVERIFIED`、`BUDGET_EXCEEDED`、 -`TRANSIENT_AUTOMATION`。 - -服务端只允许同一有效 claim 下处于 `EXECUTING` 的授权和 `PREPARING` dry-run 失败, -事务内把授权置为 `FAILED` 并保存代码、说明和时间。相同 code/message 可安全重放, -不同失败内容返回幂等冲突。任务保留 `WAITING_CONFIRMATION`,Admin 可重新选择候选并创建 -新授权;失败不会获得提交订单或支付资格。 - -### `POST /api/v1/tasks/{task_id}/order-submissions/start` - -T-218 App 在最终提交前调用。请求带 BUYER bearer、claim token、 -`Idempotency-Key`,body 固定 device、execution、generation、authorization、 -command id/hash、dry-run id/hash 和 App 重新核验的 SKU/数量/单价/总额。服务端要求 -有效租约、未取消、authorization=`EXECUTING`、dry-run=`READY` 且所有快照完全一致。 - -成功事务内创建一个 `order_submission`、写 `ORDER_SUBMISSION_FENCED` 并使原授权 -不可再次创建 submission。响应返回稳定 submission ID 和 `fenced_at`。同 key/body -重放返回相同记录;同 key 不同 body、第二个 submission 或非 READY 返回冲突。围栏 -成功只表示 App 获得过一次点击机会;从此即使响应丢失或 App 退出也只能对账,不能 -再次提交。 - -### `POST /api/v1/tasks/{task_id}/order-submissions/{submission_id}/reconcile` - -App 唯一识别待付款订单并先上传受控 evidence 后,提交订单编号、平台显示下单时间、 -标题、SKU、数量、金额、状态和 evidence asset/hash。服务端核对原 device/task/ -execution/generation、submission、期望快照、证据归属和幂等键;成功将 submission -置为 `RECONCILED`、authorization 置为 `CONSUMED` 并记录 -`order_submitted=true`。订单编号不得进入 URL、日志、事件 message 或模型字段。 -围栏创建后允许原 user/device/generation/claim token 在租约到期后完成该 submission -的对账,但任务必须仍未取消且 execution 未结束;该例外不适用于 start,也不产生 -新的提交资格。 - -### `POST /api/v1/tasks/{task_id}/order-submissions/{submission_id}/manual-review` - -围栏后无法唯一回读订单时,App 用相同身份提交受限 reason code 和可选 evidence。 -服务端将 submission 置为 `MANUAL_REVIEW` 并写审计事件,但不释放围栏、不允许生成 -第二个 submission。后续只允许人员或同一设备重新对账,不提供“重试提交”接口。 - -三个接口都不接收支付方式、支付密码或付款结果,也不授权客户端点击付款控件。 - -Admin `GET /api/v1/tasks/{task_id}` 在 T-219 增加 `order_submissions` 数组。数组按 -`fenced_at`、ID 升序;包含 submission/authorization/dry-run/execution 身份、状态、 -预期标题/SKU/数量/单价/总额和围栏时间。`RECONCILED` 项另外返回订单号、平台下单 -时间、`PENDING_PAYMENT`、对账 evidence asset/hash 和对账时间; -`MANUAL_REVIEW` 返回受限 reason code。响应必须 `Cache-Control: no-store`,订单号 -不进入任何资源 URL。无记录时返回 `[]`,不能省略字段或返回 `null`。 - -## 设备与领取 - -### `POST /api/v1/devices/heartbeat` - -App 空闲或运行时上报设备状态;运行时任务续租使用任务专用 heartbeat。 - -```json -{ - "app_version": "0.1.0", - "android_version": "16", - "pdd_version": "device-observed-value", - "readiness": { - "accessibility_enabled": true, - "pdd_installed": true, - "active_task_id": null - } -} -``` - -`device_id` 可以省略;若提供,必须与 Bearer token 中的设备一致。服务端时间是唯一 -租约时钟。响应返回服务端认定的 `active_task_id`、`client_state_matches`、 -readiness 上报时间和 `server_time`。heartbeat 超过配置 TTL 或任一就绪位为 false -时,claim 返回 `409 DEVICE_NOT_READY`。 - -### `POST /api/v1/tasks/claim-next` - -由用户点击触发,在单个 SQLite immediate transaction 中领取下一条任务。App 必须 -在请求前生成并安全保存相互独立的 256 bit Raw URL `X-Claim-Token` 和 -`Idempotency-Key`;网络结果不确定时复用同一组值。服务端只保存 token 的 -SHA-256,响应和日志都不回显原 token 或 hash。 - -```json -{} -``` - -有任务时: - -```json -{ - "task": { - "id": "37c9c715-9b51-4ed5-984e-66dad2710c71", - "status": "CLAIMED", - "title": "黑色双肩包", - "sku": "BLACK-20L", - "description": "容量约20L,外观接近参考图", - "image_asset_id": "2bbc1bf2-30f7-497e-a52b-bcb4c61d57c3", - "reference_image_sha256": "64-char-lowercase-hex", - "task_content_sha256": "64-char-lowercase-hex", - "reference_image_url": "/api/v1/tasks/37c9c715-9b51-4ed5-984e-66dad2710c71/reference-image?claim_generation=1", - "quantity": 2, - "max_budget": "200.00", - "currency": "CNY", - "version": 2, - "claim_generation": 1, - "claim_issued_at": "2026-07-25T08:30:00Z", - "claim_expires_at": "2026-07-25T08:40:00Z" - }, - "replayed": false, - "server_time": "2026-07-25T08:30:00Z" -} -``` - -`task_content_sha256` 是服务端对不可变任务内容生成的版本化摘要,客户端视为 opaque -并在候选和终态结果中原样回显;服务端拒绝与当前 execution 快照不一致的摘要。 -App 下载参考图后必须独立校验 `reference_image_sha256`。 - -当前设备自己有已过期 `CLAIMED` 时优先回收该任务,避免设备唯一归属冲突;否则按 -`created_at ASC, id ASC` 选择 `PENDING` 或已过期 `CLAIMED`。没有任务返回 `204`; -同 key 的无任务重放始终保持 `204`。同 key、同 token 的活跃 claim 重放返回同一 -任务,即使状态已进入 `RUNNING/WAITING_CONFIRMATION`;请求不同返回 -`409 IDEMPOTENCY_CONFLICT`;原 claim 已释放、取消或被其他领取回收时返回 -`409 CLAIM_REPLAY_EXPIRED`。同一设备不能再领取第二条活跃任务。 - -### `GET /api/v1/tasks/{task_id}/reference-image` - -claim 响应给出的受保护参考图地址。请求使用 BUYER Bearer token、 -`X-Claim-Token` 和 URL 中的 `claim_generation`;只有当前用户、当前设备、匹配 -generation/token 且租约未过期的活跃任务可以读取。成功返回匿名规范化 JPEG, -设置 `private, no-store`、`nosniff`、长度和 ETag,不返回存储路径。 - -### `POST /api/v1/tasks/{task_id}/start` - -把当前设备持有的 `CLAIMED` 任务改为 `RUNNING` 并创建 execution。请求带 -`X-Claim-Token` 和 `Idempotency-Key`。 - -```json -{ - "claim_generation": 1, - "expected_version": 2 -} -``` - -同 key、同请求重放返回同一 execution;错误用户/设备/token 返回 `403`,过期租约、 -状态或版本冲突返回 `409`。成功响应包含更新后的 `task`、`execution`、`replayed` -和 `server_time`。默认 running lease 为 30 分钟(允许配置 5 至 120 分钟), -`execution.execution_expires_at` 是 App 可以离线继续自动化的上限,不是后台自动 -重分配时间。 - -### `POST /api/v1/tasks/{task_id}/heartbeat` - -运行时续租并返回是否请求取消: - -```json -{ - "execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f", - "claim_generation": 1, - "step": "SCAN_RESULTS" -} -``` - -```json -{ - "task": { - "id": "37c9c715-9b51-4ed5-984e-66dad2710c71", - "status": "RUNNING", - "version": 4, - "claim_generation": 1, - "claim_expires_at": "2026-07-25T09:03:30Z" - }, - "execution": { - "id": "e3190742-a24b-441e-b5c1-c7ed10ed342f", - "current_step": "SCAN_RESULTS", - "order_submitted": false, - "execution_expires_at": "2026-07-25T09:03:30Z" - }, - "cancel_requested": false, - "server_time": "2026-07-25T08:33:30Z" -} -``` - -只接受 1 至 64 字节的大写 ASCII step。heartbeat 使用服务端 UTC 更新 execution、 -设备最近在线时间、任务 version 和 `execution_expires_at`,不写高频任务事件。 -App 每 30 秒 best-effort 调用;网络失败时可执行到上一次服务端截止时间。截止时间 -到达后 App 持久化 `SAFE_STOPPED` 并停止外部动作,任务保持原非终态且绝不回到 -领取队列。原设备重连后可以用同一 execution/claim heartbeat 同步状态;后端不延长 -已经过期的授权,且此时只接受 `step=SAFE_STOPPED`。若响应包含取消请求,App 可以 -继续调用 `cancel-ack`;没有取消时仍保持安全停止,不自动恢复采购。 - -### `POST /api/v1/tasks/{task_id}/release` - -只允许尚未开始的 `CLAIMED` 任务释放回 `PENDING`。运行中使用取消/失败流程。 -请求带 `X-Claim-Token`、`Idempotency-Key`,body 与 start 相同。成功后清除当前 -用户、设备、token hash 和租约,但保留递增过的 `claim_generation` 供审计。 - -### `POST /api/v1/tasks/{task_id}/cancel-ack` - -App 收到取消请求并在安全检查点停止后调用: - -```json -{ - "execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f", - "claim_generation": 1, - "expected_version": 5 -} -``` - -请求带 `X-Claim-Token` 和 `Idempotency-Key`。只有匹配的未结束 execution、设备 -归属和已存在的管理取消请求可以确认;App 已在安全检查点停止时,即使离线授权刚 -过期也允许原设备补交确认。成功把任务置为 `CANCELED`、结束 execution、清除 claim -秘密并追加带用户/设备 actor 的事件。同 key 重放不产生第二个事件。 - -## App 本地 AI 边界 - -MVP 后端不实现 `/api/v1/tasks/{task_id}/ai/*`,不保存 VLM 配置或 Key,也不代理 -第三方模型。App 使用本机配置的 OpenAI 兼容 adapter 完成需求提取和候选评估;后台 -任务 payload 不能携带或覆盖 provider、Base URL、model、prompt 或 API Key。 - -App 支持 `MANUAL_FIRST` 和 `AI_ASSISTED`。execution 结果必须记录实际模式; -`AI_ASSISTED` 还要记录 provider ID、model、prompt/schema version、reference/ -candidate evidence SHA-256 和结构化模型判断。结果不得包含 Key、Authorization、 -完整 endpoint、订单号、店铺名或供应商原始响应正文。 - -`sku`、`quantity` 和 `max_budget` 始终来自原任务,模型不能覆盖。App 按 ordinal -串行评估,每个 execution 最多一次需求提取、最多 5 次候选评估,并由本地确定性规则 -产生建议。模型不能返回页面动作、建议 ordinal、人工确认状态或订单授权;低置信度、 -无效 schema、证据不足或预算不确定时转人工。 - -## 执行事件与结果 - -App 使用加密 outbox 按“事件 -> evidence asset -> 候选 -> 人工 review -> 终态” -顺序提交。所有写接口 -重新校验 BUYER/device/task/execution/claim 和幂等键。原设备可以在 -`execution_expires_at` 后补报授权内已经产生的结果;后端记录 -`received_after_execution_expiry=true`,但这不允许 App 在过期后继续自动化。 - -### `POST /api/v1/tasks/{task_id}/events` - -批量追加事件,必须带 `Idempotency-Key`: - -```json -{ - "execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f", - "claim_generation": 1, - "events": [ - { - "event_id": "client-generated-uuid", - "step": "SEARCH", - "type": "STEP_COMPLETED", - "message": "已进入搜索结果页", - "occurred_at": "2026-07-25T08:34:00Z" - } - ] -} -``` - -`message` 不能包含凭证或完整个人敏感信息;同一 `event_id` 重放不重复插入。 - -### `POST /api/v1/tasks/{task_id}/evidence` - -以原始 `image/jpeg`、`image/png` 或 `image/webp` body 上传一张受控截图;请求必须带 -`Authorization: Bearer`、`X-Claim-Token`、`Idempotency-Key`、`X-Execution-ID` 和 -`X-Claim-Generation`。服务端规范化存为 JPEG 并返回 asset ID、SHA-256、尺寸及 -`received_after_execution_expiry`,不接受图片 URL,也不从第三方下载图片。 - -### `POST /api/v1/tasks/{task_id}/candidates` - -保存当前 execution 已累计的 `0..5` 个原始曝光候选快照,必须带 -`Idempotency-Key`。同一 execution 可以提交多个未完成快照,但候选只能按 ordinal 和 -`goods_id` 单调追加;完成快照不可被旧请求覆盖。正式参考图检索使用固定审计值 -`PDD_IMAGE_SEARCH`,不把标题或 SKU 伪装成图片检索词: - -```json -{ - "execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f", - "claim_generation": 1, - "task_content_sha256": "64-char-lowercase-hex", - "execution_mode": "AI_ASSISTED", - "search_query": "PDD_IMAGE_SEARCH", - "collection_complete": false, - "search_round": 1, - "provenance": { - "provider_id": "device-configured-provider", - "model": "device-configured-model", - "prompt_version": "candidate-evaluation-v2", - "schema_version": 2 - }, - "candidates": [ - { - "ordinal": 1, - "title": "页面可见标题", - "sku_text": "BLACK-L", - "price": "189.00", - "sku_verification_status": "MATCH", - "price_verification_status": "VERIFIED", - "value_source": "LOCAL_VLM", - "match_type": "EXACT_MATCH", - "search_round": 1, - "result_rank": 2, - "goods_id": "190887498", - "product_url": "https://mobile.yangkeduo.com/goods.html?goods_id=190887498", - "image_url": "", - "card_signature": "64-char-lowercase-hex", - "detail_signature": "64-char-lowercase-hex", - "detail_evidence_sha256": "detail-asset-64-char-lowercase-hex", - "specification_evidence_sha256": "specification-asset-64-char-lowercase-hex", - "evidence_asset_ids": [ - "7b733922-f90f-4bc4-a9ad-3e8ec4769122", - "4095ea37-eb4f-47c7-989b-adf060e45a32" - ], - "evaluation": { - "decision": "REVIEW", - "score": 0.82, - "matched": ["颜色和尺码均有可见证据"], - "missing_or_uncertain": [], - "rejection_reasons": [], - "confidence": 0.78, - "hard_constraints": [ - { - "kind": "COLOR", - "expected": "BLACK", - "status": "MATCH", - "evidence": "候选页面显示黑色" - }, - { - "kind": "SIZE", - "expected": "L", - "status": "MATCH", - "evidence": "候选页面显示 L 码" - } - ] - } - } - ], - "recommendation": null -} -``` - -`collection_complete=false` 表示 Roubao 仍在搜索,后台保存候选并保持任务 -`RUNNING`;此时 `candidates` 至少包含一个候选且不能提交 recommendation。 -`collection_complete=true` 才在同一事务中生成决策数据集;非空完成快照还会记录 -`CANDIDATES_READY` 并将任务切换到 `WAITING_CONFIRMATION`。完成快照不要求凑满固定 -数量:对于带 `match_type` 的已解析候选,接收 `1..5` 个且批次中每项都必须标注; -零候选完成快照仍表示搜索完成但无匹配。`EXACT_MATCH` 最多 5 个且只能来自第一轮 -SKU 硬匹配;`FALLBACK` 最多 2 个且只能来自第二轮图片搜索默认排序,必须保留 -`result_rank` 且不能作为自动推荐。累计快照可同时包含第一轮严格候选和第二轮保底候选, -但总数仍不得超过 5;后台拒绝未全部标注、超过上述上限或轮次归属不一致的快照。 -新请求的 `search_round` 为 `1..2`;升级前已经写入数据库的第三轮记录只作为历史审计读取。 - -新 App 必须为每个候选提交规格/价格审计状态:`sku_verification_status` 为 -`MATCH|MISMATCH|UNVERIFIED`,`price_verification_status` 为 -`VERIFIED|UNVERIFIED`,`value_source` 为 `DETERMINISTIC|LOCAL_VLM`。SKU 或价格为空时 -对应状态必须为 `UNVERIFIED`;第二轮 `FALLBACK` 的两项状态必须都是 `UNVERIFIED` 且 -来源固定为 `DETERMINISTIC`。后端为旧候选读取默认 `UNVERIFIED/DETERMINISTIC`,Admin -同时展示值、状态和来源。`LOCAL_VLM` 不代表金额由模型生成:金额仍须由 App 从模型引用的 -当前节点原文确定性解析。 - -`MANUAL_FIRST` 时 `provenance` 和 `evaluation` 为空,但候选观察、搜索审计值和人工 -结果仍可提交。`AI_ASSISTED` 为每个原始 observation 保存 evaluation;颜色/尺码状态 -可以是 `MATCH/MISMATCH/UNKNOWN`,模型拒绝项也必须保留。schema v2 的 -`recommendation` 只能指向颜色和尺码均为 `MATCH`、分数和置信度均不低于 `0.75` -且没有拒绝原因的原始 ordinal;没有满足项时只省略 recommendation,不能删除原始 -候选。最终快照到达时,后端在同一事务内写入 search run、observation、model -evaluation 和 recommendation,并保留最新累计 JSON 审计副本。每个非空候选必须按 -`DETAIL`、`SPECIFICATION` 顺序绑定两个不同的 evidence asset ID,且两个声明哈希 -必须分别等于后端保存的实际 asset 哈希;卡片和详情语义签名也必须是小写 SHA-256。 -后端用版本化的 execution、原 ordinal、`goods_id`、详情签名和规格证据哈希生成稳定的 -execution-scoped `candidate_key`。Admin 任务详情在 observation 的 `identity` 中 -返回该 key、四个指纹及 identity 版本;旧 v6 observation 可没有 identity。 -`goods_id` 只能是 6-20 位非零开头数字,`product_url` 必须逐字等于 -`https://mobile.yangkeduo.com/goods.html?goods_id=`;两者只能同时为空或 -同时有效。后端不请求该 URL,主要图片证据必须是已鉴权 asset。无商品身份的观察可 -留作审计,但 Admin 不允许选择它创建自动下单授权。 - -### `POST /api/v1/tasks/{task_id}/human-reviews` - -采购员确认候选后、提交终态前调用。请求使用设备 Bearer token、`X-Claim-Token` -和 `Idempotency-Key`: - -```json -{ - "execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f", - "claim_generation": 1, - "task_content_sha256": "64-char-lowercase-hex", - "reason_schema_version": 1, - "outcome": "CANDIDATE_ACCEPTED", - "selected_candidate_ordinal": 2, - "primary_reason_code": "SELECTED_BEST_MATCH", - "note": "", - "supersedes_review_id": null, - "items": [ - { - "candidate_ordinal": 1, - "label": "REJECT", - "primary_reason_code": "NOT_BEST_MATCH", - "reason_codes": ["NOT_BEST_MATCH"], - "note": "" - }, - { - "candidate_ordinal": 2, - "label": "ACCEPT", - "primary_reason_code": "SKU_MATCH", - "reason_codes": ["SKU_MATCH"], - "note": "" - } - ] -} -``` - -非空 review 必须恰好覆盖本次所有 observation;接受时只有所选 ordinal 为 -`ACCEPT`,其余全部为 `REJECT`。零候选只允许 `NO_MATCH` 或 -`MANUAL_REQUIRED` 且 items 为空。理由使用 T-208 版本 1 allowlist;任务没有预算时 -禁止价格类理由,`OTHER` 必须带 4-200 字备注。同一幂等键重放返回原 review; -修订必须通过 `supersedes_review_id` 引用当前最新版本,服务端追加版本并保留历史。 - -### `POST /api/v1/tasks/{task_id}/complete` - -人员完成确认后调用,必须带 `Idempotency-Key`: - -```json -{ - "execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f", - "claim_generation": 1, - "task_content_sha256": "64-char-lowercase-hex", - "execution_mode": "AI_ASSISTED", - "outcome": "CANDIDATE_ACCEPTED", - "operator_reason": "款式和预算符合验证要求", - "candidate": { - "ordinal": 1, - "title": "页面可见标题", - "sku_text": "BLACK-L", - "price": "189.00", - "product_url": "", - "image_url": "", - "card_signature": "64-char-lowercase-hex", - "detail_signature": "64-char-lowercase-hex", - "detail_evidence_sha256": "detail-asset-64-char-lowercase-hex", - "specification_evidence_sha256": "specification-asset-64-char-lowercase-hex", - "evidence_asset_ids": [ - "7b733922-f90f-4bc4-a9ad-3e8ec4769122", - "4095ea37-eb4f-47c7-989b-adf060e45a32" - ], - "evaluation": null - }, - "order_submitted": false -} -``` - -`outcome` 允许: - -- `CANDIDATE_ACCEPTED` -- `CANDIDATE_REJECTED` -- `NO_MATCH` -- `MANUAL_REQUIRED` - -后端对 MVP 强制 `order_submitted=false`,成功后任务进入 `SUCCEEDED`;这里的成功表示 -验证工作流正常结束,业务结果由 `outcome` 表达。 - -T-207 第一版要求 `operator_reason` 为人员输入的简短审计说明,但不把它当作可训练 -标签。T-208 在第一版链路跑通后扩展为版本化 `human_review` 合约,至少包含 -`reason_schema_version`、可空选择候选、逐候选 `ACCEPT/REJECT`、主要理由码、 -附加理由码和受限备注。改选必须同时提交原推荐项拒绝理由与替代项选择理由;全部 -无匹配必须覆盖每个曝光候选。具体 endpoint 和 schema 在领取 T-208 时冻结。 - -模型评估理由、确定性推荐理由和 `human_review` 分开保存。服务端不接受客户端把 -模型理由标记为已由人员确认;第三方商品/图片 URL 只作为受限观测字段,不触发后端 -下载,主要证据仍通过鉴权 asset API 上传。 - -### `POST /api/v1/tasks/{task_id}/fail` - -```json -{ - "execution_id": "e3190742-a24b-441e-b5c1-c7ed10ed342f", - "claim_generation": 1, - "error": { - "code": "PDD_RISK_CONTROL", - "message": "检测到平台风险提示,已停止自动化", - "step": "SCAN_RESULTS", - "retryable": false - }, - "evidence_asset_ids": ["7b733922-f90f-4bc4-a9ad-3e8ec4769122"] -} -``` - -后端只接受文档登记的错误码族和合法状态迁移。 - -## 待实现前固定 - -- 上传大小、像素和保留期限的具体数值。 -- VLM `confidence` 阈值、模型和提示词版本记录格式。 -- 外部管理后台的服务账号认证方式和调用频率。 + +> 同步来源:[`docs/routes.md`](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/docs/routes.md) · commit `afc651f75a3a` + +# 路由与页面结构 + +T-202 的离线 P0 页面入口见[原型索引](/chengma/mroubao/src/commit/afc651f75a3abc2676bb13aa8a80f6aa6a25a72e/docs/design/index.html)。原型仅用于确认页面区块、 +控件、文案和主要动作,不代表正式路由实现。 + +## 管理 Web + +| 路由 | 页面 | 职责 | 用户故事 | 交互 | +| --- | --- | --- | --- | --- | +| `/login` | 管理登录 | 建立服务端管理会话 | US-007 | IX-001 | +| `/tasks` | 任务列表 | 查看状态、筛选并进入详情 | US-002 | IX-003 | +| `/tasks/new` | 新建任务 | 提交图片和采购约束 | US-001 | IX-002 | +| `/tasks/{id}` | 任务详情 | 查看输入/候选/证据、创建下单授权,并展示对账中/人工对账/待人工付款状态 | US-002、US-006、US-010 | IX-003、IX-008、IX-011 | +| `/freight` | ERP 货运列表 | 查看已导入货运单并进入全部商品明细 | US-011 | IX-012 | +| `/freight/import` | ERP 货运导入 | 创建精确单号、最多 7 天日期范围或“同步至现在”任务,并查看成功水位 | US-011 | IX-012 | +| `/freight/{id}` | ERP 货运详情 | 查看全部商品明细、来源变化、补图和采购任务生成状态 | US-011、US-012 | IX-012、IX-013 | + +MVP 登录后默认进入 `/tasks`。未登录访问受保护页面时跳转 `/login` 并携带安全的 +站内返回路径;只接受 `/tasks`、`/freight` 及其本站子路径,拒绝绝对 URL、`//` +和反斜杠。 +不存在和无权限必须使用不同内部原因,但页面均不得泄露任务内容。 + +## Android 页面 + +Android 导航名称是逻辑目的地,具体 Compose/Fragment 形式待接入 Roubao 源码后确定。 + +| 目的地 | 页面 | 职责 | 用户故事 | 交互 | +| --- | --- | --- | --- | --- | +| `login` | 登录/设备绑定 | 建立人员与设备身份 | US-007 | IX-004 | +| `tasks` | 任务主页 | 就绪检查、获取或继续任务 | US-003 | IX-005 | +| `task/{id}/preview` | 任务预览 | 检查原始要求并开始 | US-003、US-004 | IX-005、IX-006 | +| `task/{id}/running` | 任务执行 | 显示步骤、返回拼多多、安全停止 | US-004、US-006 | IX-006、IX-008 | +| `task/{id}/confirm` | 候选确认 | 接受、拒绝、无匹配或转人工;T-208 增加逐候选结构化理由 | US-005、US-008 | IX-007、IX-009 | +| `task/{id}/result` | 任务结果 | 查看终态和同步状态 | US-002、US-006 | IX-007、IX-008 | +| `settings` | 设备设置 | 查看权限、后端、执行模式和本地 VLM 配置 | US-003、US-007、US-009 | IX-004、IX-005、IX-010 | + +## App 后端路由 + +以下路由只接受有效 BUYER Bearer token,认证 principal 中的用户和设备是权威身份: + +| 路由 | 职责 | +| --- | --- | +| `POST /api/v1/devices/heartbeat` | 上报版本、就绪位并核对服务端活跃任务 | +| `POST /api/v1/tasks/claim-next` | 用户点击后原子领取或重放领取结果 | +| `GET /api/v1/tasks/{id}/reference-image` | 用当前 claim 读取该任务匿名参考图 | +| `POST /api/v1/tasks/{id}/start` | `CLAIMED -> RUNNING` 并创建 execution | +| `POST /api/v1/tasks/{id}/heartbeat` | 更新 step、设备在线时间和运行租约 | +| `POST /api/v1/tasks/{id}/release` | 未开始时 `CLAIMED -> PENDING` | +| `POST /api/v1/tasks/{id}/cancel-ack` | 安全停止后确认 `CANCELED` 并结束 execution | +| `POST /api/v1/tasks/{id}/events` | 幂等补报本地执行事件 | +| `POST /api/v1/tasks/{id}/candidates` | 保存最多 5 个候选、模型判断和本地推荐 | +| `POST /api/v1/tasks/{id}/commands/next` | 拉取或重放 Admin 已创建的同一条下单命令 | +| `POST /api/v1/tasks/{id}/commands/{command_id}/ack` | App 加密落盘后幂等确认命令 | +| `POST /api/v1/tasks/{id}/order-dry-runs/start` | 加密保存执行意图后开始已授权商品重新定位 | +| `POST /api/v1/tasks/{id}/order-dry-runs/{command_id}/ready` | 回传确认订单页前的 SKU/数量/金额与证据 | +| `POST /api/v1/tasks/{id}/complete` | 提交人工结果,强制 `order_submitted=false` | +| `POST /api/v1/tasks/{id}/fail` | 提交结构化失败和受控证据 | + +claim/start/release/cancel-ack 使用相应幂等规则;参考图、start、task heartbeat、 +release 和 cancel-ack 都必须匹配当前 `X-Claim-Token` 与 `claim_generation`。 +运行授权过期后只有 task heartbeat 状态同步和已请求取消的 cancel-ack 允许原设备 +继续调用;它们不赋予 App 恢复外部动作的权限。 +T-207 的 events/candidates/complete/fail 接受原设备对授权内已产生结果的离线补报, +并由服务端记录是否在执行授权过期后收到;后端不提供 VLM 代理路由。 + +## 导航规则 + +- App 有活跃任务时启动后优先恢复该任务,不允许直接领取下一条。 +- `CLAIMED` 进入预览;`RUNNING` 进入执行;`WAITING_CONFIRMATION` 进入等待 Admin; + 终态进入结果。 +- 从执行页切换拼多多后,使用前台服务通知返回执行页。 +- 候选确认页返回不能恢复自动化。 +- 登录过期时先保存非敏感本地状态,再跳转登录;重新登录后查询服务端状态。 +- 设备设置不是首屏,任务主页是采购人员的工作入口。 +- 后台任务不能携带或修改 provider、模型、Base URL 和 API Key;App 设置由设备 + 操作者管理,Key 使用 Keystore-backed 加密存储。 +- 管理后端离线时执行页显示授权截止时间和待同步数量;到期后只能安全停止和等待 + 联网,不能领取或继续页面动作。 + +## 页面组件边界 + +| 组件 | 归属 | 说明 | +| --- | --- | --- | +| `TaskForm` | 管理 Web | 只管理表单状态和可访问反馈,创建逻辑在 usecase | +| `TaskStatusBadge` | 管理 Web | 统一状态中文和语义,不内置状态迁移 | +| `ExecutionTimeline` | 管理 Web/App | 展示只追加事件 | +| `ReadinessPanel` | Android | 汇总权限、安装、网络和活跃任务 | +| `TaskRequirementSummary` | Android/Web | 展示原始输入和 AI 派生信息,视觉上明确区分 | +| `CandidateReview` | Android/Web | 展示候选与硬约束校验,不执行自动化动作 | +| `ExecutionController` | Android | UI 命令入口,委托 workflow,不直接调用 Accessibility | +| `PendingPaymentSummary` | 管理 Web/App | 只读展示对账订单与人工付款提醒,不提供付款或重提动作 | +| `FreightSyncForm` | 管理 Web | 创建精确单号/日期同步记录、查看成功水位,不持有 ERP 凭证 | +| `FreightItemReview` | 管理 Web | 显示规范化来源字段、缺失项和采购任务生成操作 | + +具体状态反馈以[交互清单](/chengma/mroubao/wiki/08-interaction-checklist.-)为准,组件命名可在接入真实框架后 +调整并同步本文。