# 查价系统 技术设计 v1.2 | 版本 | 日期 | 作者 | 说明 | |------|------|------|------| | v1.0 | 2026-06-16 | 全栈架构师/Tech Lead | 可直接用于 Cursor 编码、建库、启动项目 | | v1.1 | 2026-06-16 | 全栈架构师 | 对齐 PRD v0.4:四档报价、地址联想、托盘数、嵌入宿主、Mothership 官方限制 | | v1.2 | 2026-06-17 | 全栈架构师/SRE | 对齐 PRD v0.6:QuotePageAdapter、多 URL 探测、storageState、Selector env、probe 门禁 | | v1.3 | 2026-07-13 | 全栈架构师 | 对齐 PRD v0.7:新增 §7.6 Flock Freight RPA 模块(硬限、两档映射、独立 env) | > 配套文档:[查价系统-PRD.md](./查价系统-PRD.md)(产品需求、异常设计、验收标准)· [查价系统-UI设计.md](./查价系统-UI设计.md)(页面、交互、设计规范) > 本文档为技术实现唯一依据;与 PRD 冲突时以 PRD 业务规则为准,技术实现细节以本文档为准。当前实现权威版本 **v1.3**(在 v1.2 基线上增量)。 --- ## 文档约定 | 项 | 约定 | |----|------| | 技术栈 | Next.js 15 + MySQL 8.0 + Redis 7 + BullMQ + Playwright | | 部署 | Docker Compose 一键启动 | | 金额 | `DECIMAL(12,2)`,`ROUND_HALF_UP` | | 缓存优先级 | L1 `request_id`(24h) > L2 `cargo_hash`(3min) > L3 stale(30min) | | RPA | 一次询价 = 1 次 RPA,单次返回 **4 档**(standard/guaranteed × lowest/fastest) | | 嵌入 | 查价 API/组件由**宿主系统**调用;本期仅 **admin 管理端**登录 | | 地址 | 须 `place_id` + `selected_from_suggestions`;RPA 须点击联想项 | | 计量 | `pallet_count`(托盘数);单托 weight/dims;校验见 PRD §4.2.8 | --- ## 第 1 章 技术选型 ### 1.1 前端框架 | 方案 | 优势 | 劣势 | 上手难度 | MVP 适配性 | Cursor AI 支持度 | |------|------|------|----------|------------|------------------| | React + Vite | 灵活、生态大 | 需单独配置路由/API | 中 | 中 | 高 | | Vue 3 + Vite | 模板直观 | 全栈示例相对少 | 低 | 中 | 中 | | **Next.js 15 (App Router)** | 前后端同仓、API Routes、类型安全、部署简单 | App Router 概念需熟悉 | 低-中 | **最高** | **最高** | **最终推荐:Next.js 15 (App Router)** **理由**:与后端同仓库,Cursor 可一次生成页面 + API;社区示例最多;满足 PRD 客户端 + 管理端双入口。 ### 1.2 后端技术 | 方案 | 优势 | 劣势 | 上手难度 | MVP 适配性 | Cursor AI 支持度 | |------|------|------|----------|------------|------------------| | Node.js + Express | 轻量 | 需手动搭中间件/分层 | 中 | 中 | 高 | | Python + FastAPI | 文档自动生成 | 与 Next 异构,双仓维护 | 中 | 高 | 高 | | Java + Spring Boot | 企业级稳定 | 配置重、启动慢 | 高 | 低 | 中 | | **Next.js API Routes + 独立 Worker 进程** | 同语言、同类型、AI 生成率高 | 大规模需拆分 | **低** | **最高** | **最高** | **最终推荐:Next.js API Routes(Web)+ `workers/rpa` 独立 Node 进程(BullMQ Consumer)** **理由**:Web 层处理 HTTP/鉴权/编排;RPA 长任务独立进程,避免 Serverless 超时;非程序员只需 `docker compose up`。 ### 1.3 数据库 | 方案 | 快速建模 | AI 生成 Schema | MVP 适配 | 最终 | |------|----------|----------------|----------|------| | PostgreSQL | 高 | 高 | 高 | | | **MySQL 8.0** | 高 | **最高(Prisma 示例最多)** | **最高** | **✅** | | Supabase | 最高(托管) | 中 | 中(vendor lock-in) | | **最终推荐:MySQL 8.0 + Prisma ORM** **理由**:PRD 已定 MySQL;Prisma migration 可由 Cursor 直接生成;Docker 镜像小。 ### 1.4 ORM / 队列 / 缓存 | 组件 | 候选 | 最终推荐 | 理由 | |------|------|----------|------| | ORM | Prisma / Drizzle / TypeORM | **Prisma** | Cursor 生成成功率最高 | | 队列 | BullMQ / Bee-Queue | **BullMQ** | PRD 已定;Redis 原生 | | 缓存 | Redis / Memcached | **Redis** | 同时支撑队列 + L1/L2/L3 | | RPA | Playwright / Puppeteer | **Playwright** | PRD 已定;维护性更好 | ### 1.5 部署方案 | 方案 | 非运维友好度 | 本地=生产一致性 | 最终 | |------|--------------|-----------------|------| | Vercel | 最高 | 低(Worker/RPA 难部署) | | | Railway | 高 | 中 | | | **Docker Compose + VPS** | **高(一条命令)** | **最高** | **✅** | | AWS ECS | 低 | 高 | | **最终推荐:Docker Compose** **理由**:Next.js + MySQL + Redis + RPA Worker 四容器本地与生产一致;`docker compose up -d` 即可启动。 --- ## 第 2 章 系统架构设计 ### 2.1 总体架构 ``` ┌─────────────────────────────────────────────────────────────┐ │ 宿主业务系统(内嵌查价组件 / 服务端调用) │ │ └─ Service Token + customer_id 透传 │ └───────────────────────────┬─────────────────────────────────┘ │ HTTPS /api/* ┌───────────────────────────▼─────────────────────────────────┐ │ next-app (Next.js 15) │ │ ├─ middleware.ts 宿主鉴权 + 管理员鉴权 + 限流 │ │ ├─ app/api/quotes/* 询价 API │ │ ├─ app/api/markup-configs/* (宿主调用,无运营登录页) │ │ ├─ app/api/alerts/* 管理员 │ │ └─ app/admin/* 管理端(预警/RPA/开发进度) │ └───────────────┬─────────────────────────┬─────────────────────┘ │ │ ┌───────▼───────┐ ┌───────▼────────┐ │ MySQL 8.0 │ │ Redis 7 │ └───────────────┘ └───────┬────────┘ │ ┌─────────▼──────────┐ │ rpa-worker │ │ Playwright │ │ MothershipRPA │ │ · 地址联想点选 │ │ · 四档抓取 │ └─────────┬──────────┘ │ RPA ┌─────────▼──────────┐ │ Mothership Web │ └────────────────────┘ ``` ### 2.2 模块划分 | 模块 | 职责 | 代码路径 | |------|------|----------| | 鉴权 | JWT 签发/校验、RBAC | `modules/auth/`、`middleware.ts` | | 询价编排 | 校验→缓存→落库→入队→轮询查询 | `modules/quote/orchestrator.ts` | | 缓存 | L1/L2/L3 读写、击穿锁 | `modules/cache/redis-cache.ts` | | 幂等 | request_id 24h | `modules/quote/idempotency.ts` | | 加价 | 运费百分比计算 | `modules/pricing/engine.ts` | | 预警 | alert_log 写入、偏差检测 | `modules/alert/service.ts` | | RPA | BullMQ 消费、Playwright 抓取 | `workers/rpa/` | | 定时任务 | processing 超时、熔断恢复 | `workers/scheduler/` | ### 2.3 核心数据流(询价) ``` 1. POST /api/quotes 2. ValidationModule:格式校验 + in/lb 换算 + cargo_hash 3. IdempotencyModule:查 L1(request_id) → 命中返回 quote_id 4. CacheModule:查 L2(cargo_hash) → 命中:PricingEngine 加价 → 落库 → 写 L1 → done 5. 未命中:INSERT quote_record(processing) → BullMQ.add('quote', {quote_id}) 6. 返回 { quote_id, status: processing } --- 异步 --- 7. rpa-worker 消费 job 8. MothershipRPAProvider.getQuote() → 四档原始价(standard/guaranteed × lowest/fastest) 9. 一致性校验 → 写 L2(3min+jitter)/L3(30min) → UPDATE quote_record(done) 10. quote_cache_meta 更新 → 偏差≥5% 写 PRICE_DEVIATION 11. 写 L1(request_id) --- 客户端 --- 12. GET /api/quotes/{quote_id} 每 2s 轮询,最长 30s 13. valid_until 过期 → status=expired(查询时计算) ``` ### 2.4 Fallback 数据流 ``` RPA 抛错(超时/验证码/STRUCT_CHANGE/DATA_INVALID) → 读 L3 stale(cargo_hash) → 有:done + source_type=stale + is_realtime=false + STALE_FALLBACK 告警 → 无:failed + QUOTE_UNAVAILABLE + RPA_FAILED 告警 验证码额外:暂停 Worker 10min + RPA_CAPTCHA 告警 连续失败≥3:熔断 10min,期间不分配 RPA job ``` ### 2.5 第三方依赖 | 依赖 | 用途 | 版本 | |------|------|------| | Mothership Web | RPA 报价源 | 外部 | | Playwright | 浏览器自动化 | ^1.44 | | BullMQ | 任务队列 | ^5.x | | ioredis | Redis 客户端 | ^5.x | | Prisma | ORM | ^5.x | | jose | JWT | ^5.x | | zod | 入参校验 | ^3.x | --- ## 第 3 章 数据库设计 ### 3.1 ER 关系 ``` sys_user (1) ──< quote_record (N) markup_config (1) ── (1) customer_id 逻辑关联 quote_record (N) ── (0..1) alert_log quote_cache_meta (1) ── (1) cargo_hash idempotency_record (1) ── (1) request_id ``` ### 3.2 表:sys_user(鉴权 MVP) > **v1.1**:本期仅保留 **admin** 管理端登录;不提供 customer/operator 业务账号。查价 API 使用宿主 **Service Token**(见 §4.2)。 | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| | id | BIGINT UNSIGNED | PK | | | user_id | VARCHAR(32) | UNIQUE, NOT NULL | 对外 ID | | customer_id | VARCHAR(32) | UNIQUE, NULL | **保留字段**;宿主 token 场景不使用 | | username | VARCHAR(64) | UNIQUE, NOT NULL | 登录名 | | password_hash | VARCHAR(255) | NOT NULL | bcrypt | | role | VARCHAR(16) | NOT NULL | **admin**(本期唯一登录角色) | | is_deleted | TINYINT(1) | DEFAULT 0 | | | created_at | DATETIME | NOT NULL | | | updated_at | DATETIME | NOT NULL | | 索引:`uk_user_id`、`uk_username`、`uk_customer_id` ### 3.3 表:quote_record 与 PRD §9.1 一致,见下方 Prisma + SQL。 ### 3.4 表:idempotency_record / quote_cache_meta / markup_config / alert_log 与 PRD §9.2–9.5 一致。 ### 3.5 Prisma Schema(可直接 `prisma migrate dev`) ```prisma generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") } model SysUser { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt userId String @unique @map("user_id") @db.VarChar(32) customerId String? @unique @map("customer_id") @db.VarChar(32) username String @unique @db.VarChar(64) passwordHash String @map("password_hash") @db.VarChar(255) role String @db.VarChar(16) isDeleted Boolean @default(false) @map("is_deleted") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@map("sys_user") } model QuoteRecord { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt quoteId String @unique @map("quote_id") @db.VarChar(32) requestId String @map("request_id") @db.VarChar(36) customerId String @map("customer_id") @db.VarChar(32) cargoHash String @map("cargo_hash") @db.Char(32) status String @default("processing") @db.VarChar(16) sourceType String? @map("source_type") @db.VarChar(16) isRealtime Boolean @default(true) @map("is_realtime") confidenceScore Decimal? @map("confidence_score") @db.Decimal(3, 2) currency String @default("USD") @db.VarChar(8) pickupJson Json @map("pickup_json") deliveryJson Json @map("delivery_json") weightLb Decimal @map("weight_lb") @db.Decimal(12, 2) dimLIn Decimal @map("dim_l_in") @db.Decimal(8, 2) dimWIn Decimal @map("dim_w_in") @db.Decimal(8, 2) dimHIn Decimal @map("dim_h_in") @db.Decimal(8, 2) palletCount Int @map("pallet_count") cargoType String @map("cargo_type") @db.VarChar(32) quotesJson Json? @map("quotes_json") markupPercent Decimal @default(0.0) @map("markup_percent") @db.Decimal(5, 1) validUntil DateTime? @map("valid_until") errorCode String? @map("error_code") @db.VarChar(32) ccCustomerId String? @map("cc_customer_id") @db.VarChar(32) forecastId String? @map("forecast_id") @db.VarChar(32) isDeleted Boolean @default(false) @map("is_deleted") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@index([customerId, createdAt], map: "idx_customer_created") @@index([cargoHash], map: "idx_cargo_hash") @@index([requestId], map: "idx_request") @@map("quote_record") } model IdempotencyRecord { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt requestId String @unique @map("request_id") @db.VarChar(36) customerId String @map("customer_id") @db.VarChar(32) quoteId String @map("quote_id") @db.VarChar(32) expireAt DateTime @map("expire_at") createdAt DateTime @default(now()) @map("created_at") @@map("idempotency_record") } model QuoteCacheMeta { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt cargoHash String @unique @map("cargo_hash") @db.Char(32) rawTotalStandard Decimal @map("raw_total_standard") @db.Decimal(12, 2) rawTotalGuaranteed Decimal @map("raw_total_guaranteed") @db.Decimal(12, 2) lastQuotesJson Json @map("last_quotes_json") refreshedAt DateTime @default(now()) @map("refreshed_at") @@map("quote_cache_meta") } model MarkupConfig { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt customerId String @unique @map("customer_id") @db.VarChar(32) markupPercent Decimal @map("markup_percent") @db.Decimal(5, 1) operatorId String @map("operator_id") @db.VarChar(32) remark String? @db.VarChar(255) isDeleted Boolean @default(false) @map("is_deleted") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@map("markup_config") } model AlertLog { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt alertType String @map("alert_type") @db.VarChar(32) quoteId String? @map("quote_id") @db.VarChar(32) cargoHash String? @map("cargo_hash") @db.Char(32) detailJson Json? @map("detail_json") status String @default("open") @db.VarChar(16) resolverId String? @map("resolver_id") @db.VarChar(32) resolvedAt DateTime? @map("resolved_at") createdAt DateTime @default(now()) @map("created_at") @@index([alertType, status], map: "idx_type_status") @@index([createdAt], map: "idx_created") @@map("alert_log") } ``` ### 3.6 Redis Key 设计 | Key | 值 | TTL | 说明 | |-----|-----|-----|------| | `idem:{request_id}` | `{quote_id, response}` | 24h | L1 幂等 | | `quote:{cargo_hash}` | 原始四档 JSON(未加价) | 3min + 0~30s jitter | L2 热缓存 | | `stale:{cargo_hash}` | 同 L2 结构 | 30min | L3 降级 | | `lock:quote:{cargo_hash}` | `1` | 5s | 击穿锁 | | `ratelimit:customer:{id}` | 计数 | 60s | 60次/分钟 | | `circuit:rpa` | `open/closed` | 10min | 熔断状态 | ### 3.7 cargo_hash 算法 ```typescript import { createHash } from 'crypto'; export function buildCargoHash(input: { pickup: string; // 含 place_id 的标准化 JSON delivery: string; weightLb: string; // 单托重量,2 位小数 dimLIn: string; dimWIn: string; dimHIn: string; palletCount: number; cargoType: string; }): string { const raw = [ input.pickup, input.delivery, input.weightLb, input.dimLIn, input.dimWIn, input.dimHIn, String(input.palletCount), input.cargoType, ].join('|'); return createHash('md5').update(raw).digest('hex'); } ``` **不含** `service_level`、`rate_option`、`customer_id`。 --- ## 第 4 章 API 设计 ### 4.1 统一响应 ```typescript // 成功 { "code": 0, "message": "ok", "data": { ... } } // 失败 { "code": "VALIDATION_FAILED", "message": "请填写重量", "data": null } ``` 所有查价接口响应头:`Cache-Control: no-store` ### 4.2 鉴权 **宿主查价 API**: ``` Authorization: Bearer X-Customer-Id: CUST_001 // 或由请求体 customer_id 携带,须与 token 租户绑定 ``` **管理端 API**: ``` Authorization: Bearer JWT payload: { sub: user_id, role: "admin", exp } ``` | 调用方 | 权限 | |--------|------| | 宿主 Service Token | POST/GET quotes、GET history、PUT markup-configs(若授权) | | admin | GET alerts、POST alerts resolve、GET rpa/status(开发进度) | > 本期**无** customer/operator 独立 JWT 登录;`POST /api/auth/login` 仅用于管理端 admin。 ### 4.3 接口清单 #### POST /api/quotes | 项 | 内容 | |----|------| | 权限 | 宿主 Service Token | | 请求体 | 见 PRD §4.3 | | 成功 200 | `{ quote_id, status: "processing" \| "done" }` | | 错误 | 400 VALIDATION_FAILED、401 UNAUTHORIZED、429 RATE_LIMITED | #### GET /api/quotes/{quote_id} | 项 | 内容 | |----|------| | 权限 | 宿主(本人 customer_id) | | 成功 200 | 完整 QuoteResponse(PRD §5.2);`valid_until < now` 时 `status=expired` | | 错误 | 403 FORBIDDEN、404 QUOTE_NOT_FOUND | #### GET /api/quotes/history?page=1&size=20 | 项 | 内容 | |----|------| | 权限 | 宿主 Service Token | | 必填 | page、size(缺省 400) | | 成功 200 | `{ list: QuoteSummary[], total, page, size }` | #### GET /api/markup-configs?page=1&size=20 | 项 | 内容 | |----|------| | 权限 | 宿主(`pricing:markup:read` 可选) | | 成功 200 | 分页列表 | #### PUT /api/markup-configs/{customer_id} | 项 | 内容 | |----|------| | 权限 | 宿主(`pricing:markup:write`) | | 请求体 | `{ markup_percent, remark? }` | | 成功 200 | 配置对象 | | 错误 | 400(>30%)、403 FORBIDDEN | #### GET /api/alerts?page=1&size=20&type=&status=open | 项 | 内容 | |----|------| | 权限 | admin(`alert:read`) | | 成功 200 | 分页列表 | #### POST /api/alerts/{id}/resolve | 项 | 内容 | |----|------| | 权限 | admin | | 请求体 | `{ resolver_id }` | | 成功 200 | `{ id, status: "resolved" }` | #### POST /api/auth/login(管理端 MVP) | 项 | 内容 | |----|------| | 请求体 | `{ username, password }` | | 成功 200 | `{ token, user: { user_id, role: "admin" } }` | | 说明 | **仅管理员**;不提供 customer/operator 登录 | ### 4.4 错误码 | code | HTTP | 说明 | |------|------|------| | VALIDATION_FAILED | 400 | 入参校验失败 | | UNAUTHORIZED | 401 | JWT 无效 | | FORBIDDEN | 403 | 越权/无 RBAC | | QUOTE_NOT_FOUND | 404 | quote_id 不存在 | | RATE_LIMITED | 429 | 限流 | | QUOTE_UNAVAILABLE | 200* | status=failed | | QUOTE_TIMEOUT | 200* | status=failed | | INTERNAL_ERROR | 500 | 系统异常 | \* 询价失败通过 `status=failed` + `error_code` 表达,HTTP 仍为 200。 ### 4.5 加价计算实现 ```typescript export function applyMarkup( rawFreight: number, rawTotal: number, markupPercent: number, ): { markupAmount: number; finalTotal: number } { const markupAmount = roundHalfUp(rawFreight * (markupPercent / 100), 2); const finalTotal = roundHalfUp(rawTotal + markupAmount, 2); return { markupAmount, finalTotal }; } function roundHalfUp(value: number, decimals: number): number { const factor = 10 ** decimals; return Math.round((value + Number.EPSILON) * factor) / factor; } ``` --- ## 第 5 章 项目工程结构 ``` chajia/ ├── docker-compose.yml ├── .env.example ├── package.json ├── next.config.ts ├── tsconfig.json ├── middleware.ts # JWT + 限流 ├── prisma/ │ ├── schema.prisma │ └── seed.ts ├── app/ │ ├── layout.tsx │ ├── page.tsx # 客户查价页 │ ├── login/page.tsx │ ├── admin/ │ │ ├── markup/page.tsx │ │ └── alerts/page.tsx │ └── api/ │ ├── auth/login/route.ts │ ├── quotes/route.ts │ ├── quotes/[quoteId]/route.ts │ ├── quotes/history/route.ts │ ├── markup-configs/route.ts │ ├── markup-configs/[customerId]/route.ts │ └── alerts/ │ ├── route.ts │ └── [id]/resolve/route.ts ├── modules/ │ ├── auth/jwt.ts │ ├── quote/ │ │ ├── orchestrator.ts │ │ ├── idempotency.ts │ │ ├── validation.ts │ │ └── types.ts │ ├── cache/redis-cache.ts │ ├── pricing/engine.ts │ ├── alert/service.ts │ └── providers/ │ ├── quote-provider.ts │ ├── mothership-rpa-provider.ts │ └── flock-freight-rpa-provider.ts # Phase 扩展(PRD §14) ├── workers/ │ ├── rpa/ │ │ ├── index.ts # BullMQ consumer 入口 │ │ ├── mothership.ts # Playwright 脚本 │ │ ├── flock-freight.ts # Flock get-a-quote RPA(PRD §14) │ │ ├── quote-page-adapter.ts # 多 URL 探测 + preCheck(PRD GD-15) │ │ └── session-manager.ts # BrowserContext + storageState │ └── scheduler/ │ └── timeout-sweeper.ts # processing >30s 标记失败 ├── lib/ │ ├── prisma.ts │ ├── redis.ts │ ├── response.ts │ └── constants/ │ ├── mothership-limits.ts │ └── flock-limits.ts # PRD §14.3 ├── components/ │ ├── quote-form.tsx │ ├── quote-result.tsx │ └── countdown.tsx ├── types/ │ └── api.ts └── scripts/ ├── dev.sh ├── migrate.sh ├── probe-rpa.ts # RPA 探针(TC-601 / Go/No-Go) ├── probe-flock-rpa.ts # Flock 探针(PRD §14.7) ├── record-mothership.sh # Playwright codegen 录制入口 └── record-flock.sh # Flock codegen 录制入口 ``` ### 5.1 目录职责 | 目录 | 职责 | Cursor 提示词示例 | |------|------|-------------------| | `app/api/` | HTTP 路由薄层,只调 modules | 「在 app/api/quotes/route.ts 实现 POST,调用 orchestrator」 | | `modules/` | 业务逻辑 | 「在 modules/quote/orchestrator.ts 实现 L1→L2→入队」 | | `workers/rpa/` | 长任务 | 「实现 MothershipRPAProvider,一次 RPA 返回四档 + 地址联想点选」 | | `prisma/` | 数据模型 | 「按技术设计 schema 生成 migration」 | | `components/` | UI 组件 | 「实现查价表单,含单位切换与防重复提交」 | --- ## 第 6 章 Docker Compose 与启动 ### 6.1 docker-compose.yml ```yaml services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: chajia ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 5s timeout: 5s retries: 10 redis: image: redis:7-alpine ports: - "6379:6379" volumes: - redis_data:/data next-app: build: context: . dockerfile: Dockerfile ports: - "3000:3000" environment: DATABASE_URL: mysql://root:${MYSQL_ROOT_PASSWORD}@mysql:3306/chajia REDIS_URL: redis://redis:6379 JWT_SECRET: ${JWT_SECRET} NODE_ENV: production depends_on: mysql: condition: service_healthy redis: condition: service_started rpa-worker: build: context: . dockerfile: Dockerfile.worker environment: DATABASE_URL: mysql://root:${MYSQL_ROOT_PASSWORD}@mysql:3306/chajia REDIS_URL: redis://redis:6379 MOTHERSHIP_EMAIL: ${MOTHERSHIP_EMAIL} MOTHERSHIP_PASSWORD: ${MOTHERSHIP_PASSWORD} MOTHERSHIP_QUOTE_URLS: ${MOTHERSHIP_QUOTE_URLS} RPA_STORAGE_STATE_PATH: ${RPA_STORAGE_STATE_PATH:-.rpa/mothership-storage.json} RPA_MOCK_MODE: ${RPA_MOCK_MODE:-false} RPA_HEADLESS: ${RPA_HEADLESS:-true} volumes: - rpa_state:/app/.rpa depends_on: - mysql - redis # Playwright 需要 shm shm_size: "1gb" volumes: mysql_data: redis_data: rpa_state: ``` ### 6.2 .env.example ```env MYSQL_ROOT_PASSWORD=changeme JWT_SECRET=your-256-bit-secret # RPA(PRD v0.6 GD-15,必填:headed 录制后填入) MOTHERSHIP_QUOTE_URLS= RPA_STORAGE_STATE_PATH=.rpa/mothership-storage.json RPA_MOCK_MODE=false RPA_HEADLESS=true # Selector(Playwright codegen 产出,见 §7.2.3) RPA_SELECTOR_PICKUP_STREET= RPA_SELECTOR_DELIVERY_STREET= RPA_SELECTOR_ADDRESS_SUGGESTION= RPA_SELECTOR_SUBMIT= RPA_SELECTOR_STANDARD_TAB= RPA_SELECTOR_GUARANTEED_TAB= RPA_SELECTOR_LOWEST_OPTION= RPA_SELECTOR_FASTEST_OPTION= RPA_SELECTOR_PRICE= # 可选:仅当入口跳转 login 时使用 MOTHERSHIP_EMAIL= MOTHERSHIP_PASSWORD= # 可选:Patchright 反检测(P1) RPA_USE_PATCHRIGHT=false # Flock Freight(PRD v0.7 §14 / 技术设计 §7.6;默认关闭) FLOCK_RPA_ENABLED=false FLOCK_QUOTE_URL=https://app.flockfreight.com/get-a-quote FLOCK_STORAGE_STATE_PATH=.rpa/flock-storage.json # FLOCK_SELECTOR_* 由 record-flock.sh 录制后填入 DATABASE_URL=mysql://root:changeme@localhost:3306/chajia REDIS_URL=redis://localhost:6379 ``` ### 6.3 启动命令 ```bash # 1. 复制环境变量 cp .env.example .env # 2. 启动全部服务 docker compose up -d --build # 3. 数据库迁移(首次) docker compose exec next-app npx prisma migrate deploy docker compose exec next-app npx prisma db seed # 4. 访问 # 客户查价:http://localhost:3000 # 管理端: http://localhost:3000/admin/markup ``` ### 6.4 本地开发(非 Docker) ```bash npm install docker compose up -d mysql redis npx prisma migrate dev npm run dev # Next.js :3000 npm run worker:rpa # RPA Worker ``` --- ## 第 7 章 RPA 实现规格 ### 7.1 QuoteProvider 接口 ```typescript export interface QuoteItem { serviceLevel: 'standard' | 'guaranteed'; rateOption: 'lowest' | 'fastest'; carrier: string; transitDays: string; transitDescription: string; rawFreight: number; surcharges: number; rawTotal: number; } export interface QuoteProvider { getQuote(req: QuoteRequest): Promise<{ items: QuoteItem[] }>; // items.length === 4 healthCheck(): Promise; } ``` ### 7.2 MothershipRPAProvider 执行步骤 > **匿名查价(PRD v0.6)**:通过 `QuotePageAdapter` 探测 `MOTHERSHIP_QUOTE_URLS`;**禁止**默认 `dashboard.mothership.com` 或 `www.mothership.com/quote`。`session-manager` 加载/写回 `RPA_STORAGE_STATE_PATH`;仅在 login 页且已配置凭据时重登。`RPA_MOCK_MODE` 仅 CI/单测。 1. `QuotePageAdapter.resolveQuoteEntry()`:按序 goto + preCheck,首个可用 URL 为 `activeUrl`;全失败 → `QUOTE_ENTRY_UNAVAILABLE` 2. `session-manager.createContext()`:若 storageState 存在则 `storageState: path`;成功后 `context.storageState({ path })` 3. **preCheck**:404 / login 且无凭据 → `STRUCT_CHANGE`(**非** SESSION_EXPIRED);表单 selector 不可见 → `STRUCT_CHANGE` 4. **地址(关键)**: - 若入参含 `place_id`:优先用 Mothership 已选地址回填/校验 - 否则:`RPA_SELECTOR_PICKUP_STREET` / `DELIVERY_STREET` 键入 → `waitForSelector(RPA_SELECTOR_ADDRESS_SUGGESTION)` → **click** 含 zip 项(禁止 Enter) - 断言:联想下拉消失;隐藏字段或展示文本已锁定;失败 → `ADDRESS_SUGGESTION_NOT_FOUND` 5. 填入单托 weight(lb)/dims(in)、`pallet_count`、`cargo_type` 6. `click(RPA_SELECTOR_SUBMIT)`,等待结果页 7. 在 **standard** / **guaranteed** 区块:分别点选 lowest/fastest,各抓取价格与时效 8. 校验:`rawFreight > 0`;**四档均存在** 9. 返回 `items[4]` ### 7.2.3 QuotePageAdapter 接口 ```typescript export interface QuotePageAdapter { /** 按 MOTHERSHIP_QUOTE_URLS 顺序探测,返回首个通过 preCheck 的 URL */ resolveQuoteEntry(page: Page): Promise<{ activeUrl: string }>; /** 非 login/captcha、报价表单 selector 可见 */ preCheck(page: Page): Promise; /** login 页且无 MOTHERSHIP_EMAIL/PASSWORD */ isLoginWithoutCredentials(page: Page): boolean; } ``` 实现文件:`workers/rpa/quote-page-adapter.ts` preCheck 最小检查项:`RPA_SELECTOR_PICKUP_STREET`、`RPA_SELECTOR_SUBMIT` 可见;URL 不含 `/login`;无 Cloudflare challenge DOM。 | env | 用途 | |-----|------| | `RPA_SELECTOR_PICKUP_STREET` | 发货街道输入 | | `RPA_SELECTOR_DELIVERY_STREET` | 收货街道输入 | | `RPA_SELECTOR_ADDRESS_SUGGESTION` | 联想下拉项 | | `RPA_SELECTOR_SUBMIT` | 提交按钮 | | `RPA_SELECTOR_STANDARD_TAB` | standard 区块 | | `RPA_SELECTOR_GUARANTEED_TAB` | guaranteed 区块 | | `RPA_SELECTOR_LOWEST_OPTION` | 最低价格选项 | | `RPA_SELECTOR_FASTEST_OPTION` | 最快递送选项 | | `RPA_SELECTOR_PRICE` | 价格展示节点 | ### 7.2.1 地址联想 Selector 约定(实施时以 RPA 录制为准) | 步骤 | Playwright 动作 | |------|-----------------| | 输入街道 | `fill(process.env.RPA_SELECTOR_PICKUP_STREET, street)` | | 等待下拉 | `waitForSelector(process.env.RPA_SELECTOR_ADDRESS_SUGGESTION, { timeout: 5000 })` | | 点选 | `click` 首条含 `zip` 匹配项 | | 断言锁定 | 联想下拉消失;`fill-verify` 后 hidden place_id 或 formatted 文本非空 | 失败分类:`ADDRESS_SUGGESTION_NOT_FOUND` → 不重试 → 宿主 400 或 L3 ### 7.2.2 货物校验(validation.ts) 实现 PRD §4.2.8 硬顶,**禁止**硬编码 50000 lb / 120 in: ```typescript export const MOTHERSHIP_LIMITS = { palletCount: { min: 1, max: 25 }, dimIn: { lengthMax: 999, widthMax: 99, heightMax: 99, min: 1 }, weightLbPerPallet: { min: 0.01, max: 9_999 }, totalWeightLbMax: 249_975, } as const; ``` ### 7.3 错误分类与处理 | 错误类 | 处理 | |--------|------| | QUOTE_ENTRY_UNAVAILABLE | 不重试 → L3 + STRUCT_CHANGE 告警 | | ADDRESS_SUGGESTION_NOT_FOUND | 不重试;宿主 API 400;Worker → L3 | | PAGE_LOAD_TIMEOUT | 重试 1 次 → L3 | | RPA_CAPTCHA | 停 Worker 10min → L3 | | STRUCT_CHANGE | 不重试(含 login 无凭据、404、selector 缺失)→ L3 + 告警 | | RPA_DATA_INVALID | L3 | | SESSION_EXPIRED | **仅**已配置凭据且曾成功登录时:重登 1 次 → 失败则 L3 | ### 7.5 storageState 生命周期 ```typescript // session-manager.ts 要点 const statePath = process.env.RPA_STORAGE_STATE_PATH ?? '.rpa/mothership-storage.json'; async function createContext(browser: Browser) { const opts = fs.existsSync(statePath) ? { storageState: statePath } : {}; return browser.newContext(opts); } async function persistContext(context: BrowserContext) { await context.storageState({ path: statePath }); } async function invalidateStorageState() { if (fs.existsSync(statePath)) fs.unlinkSync(statePath); } ``` - 成功匿名查价后写回 storageState - preCheck 命中 login 且无凭据:先 `invalidateStorageState()` 再重试一次 goto;仍失败 → STRUCT_CHANGE - Docker:`rpa_state` volume 挂载 `/app/.rpa` ### 7.4 BullMQ Job 结构 ```typescript interface QuoteJobData { quoteId: string; requestId: string; customerId: string; cargoHash: string; cargo: NormalizedCargo; } ``` 队列名:`quote-rpa`;并发:2;单 job 超时:25s;重试:1 次。 ### 7.6 FlockFreightRPAProvider(PRD §14 / GD-16,Phase 扩展) > **状态**:已落地(API / 校验 / 嵌入 UI / **Direct `user/quotes` JSON 优先** + DOM 回退)。生产开启须 `FLOCK_RPA_ENABLED=true`;探针 `probe:flock-rpa` / `batch:flock-direct-10`。 #### 7.6.1 职责与文件 | 文件 | 职责 | |------|------| | `modules/flock/orchestrator.ts` | 入队 `flock-quote` | | `lib/flock/direct-quote.ts` | Direct:bootstrap cookie → `POST api.flockfreight.com/user/quotes` | | `lib/flock/quote-payload.ts` / `map-fulfillment-options.ts` | 请求体与两档映射 | | `workers/rpa/flock/run-quote.ts` | `FLOCK_QUOTE_MODE`:`direct` / `dom` / `direct_then_dom`(默认) | | `workers/rpa/flock/visual-chain.ts` | DOM 填表回退路径 | | `lib/constants/flock-limits.ts` | `FLOCK_LIMITS` 硬限常量 | | `scripts/probe-flock-rpa.ts` / `batch:flock-direct-10` | 探针 | #### 7.6.2 硬限常量 ```typescript /** 来源:PRD §14.3,官网 get-a-quote 提示文案(2026-07-13) */ export const FLOCK_LIMITS = { palletCount: { min: 4, max: 20 }, totalWeightLbMax: 45_000, // 整票总重,非单托 dimIn: { lengthMax: 636, widthMax: 102, heightMax: 108, min: 1 }, zipPattern: /^\d{5}(-\d{4})?$/, } as const; ``` 校验入口:`validation.ts` 在 `provider=flock`(或独立 API 路径)时走 `FLOCK_LIMITS`,**禁止**复用 `MOTHERSHIP_LIMITS`。 计量:Flock 表单重量为**整票总重**;若宿主仍传单托重,编排层换算 `totalWeightLb = weightLb * palletCount` 后再校验与填表。 #### 7.6.3 执行步骤 **主路径 Direct(推荐)**: 1. 打开 `FLOCK_QUOTE_URL` 仅获取 `anonymous_token` / `ff_jwt` 2. `POST https://api.flockfreight.com/user/quotes`(契约见 `.rpa/flock-network-poc/`) 3. 从 `fulfillmentOptions` 映射:`GUARANTEED_HUBLESS`→FlockDirect®,`STANDARD` 最低价→Standard 4. 经 PricingEngine 加价写库;**不填表、不注册** **回退 DOM(`FLOCK_QUOTE_MODE=direct_then_dom|dom`)**: 1. 配置了 `FLOCK_LOGIN_EMAIL` / `FLOCK_LOGIN_PASSWORD` 时:**先登录**(`workers/rpa/flock/login.ts`),再填表抓价;禁止 random_qq / 盲填注册;成功可写回 `FLOCK_STORAGE_STATE_PATH` 2. 无登录凭据时:`goto(FLOCK_QUOTE_URL)`;preCheck 失败 → `QUOTE_ENTRY_UNAVAILABLE` 3. 填 **取货日期**(`FLOCK_SELECTOR_PICKUP_DATE`) 4. 填 **取件 ZIP / 投递 ZIP**(无街道联想;ZIP 非法由中台先 400) 5. 填 **托盘数量**、**总运输重量(lb)**、**单托 L/W/H(in)**(默认官网 48/40/48,可覆盖) 6. `click(FLOCK_SELECTOR_NEXT)`,等待结果页「Explore your custom quote options」 7. 解析两卡: - FlockDirect® → `{ serviceLevel:'guaranteed', rateOption:'fastest', carrier:'Flock Freight', rawFreight, transitDays }` - Standard → `{ serviceLevel:'standard', rateOption:'lowest', carrier:'Flock Freight', ... }` 8. 可选抓取 `reference`(如 `FRG-XXXX`)写入 `quote_record` 扩展字段或日志 9. 校验至少 `FLOCK_MIN_QUOTES` 档且价格 >0;否则失败 10. **停止边界**:不点击「Complete your order」(避免进入注册/下单) Direct 仍匿名 bootstrap;DOM 保障路径优先使用 `FLOCK_LOGIN_*`。自动注册仅用于无登录凭据的探针脚本。 #### 7.6.4 环境变量 | env | 必填 | 说明 | |-----|------|------| | `FLOCK_QUOTE_URL` | 是(启用时) | 默认 `https://app.flockfreight.com/get-a-quote` | | `FLOCK_QUOTE_MODE` | 否 | `direct` / `dom` / `direct_then_dom`(默认,Direct 优先) | | `FLOCK_LOGIN_EMAIL` / `FLOCK_LOGIN_PASSWORD` | DOM 保障建议 | 账号密码登录;配置后禁用无痕随机注册 | | `FLOCK_LOGIN_URL` | 否 | 默认 `https://app.flockfreight.com/login` | | `FLOCK_STORAGE_STATE_PATH` | 否 | 默认 `.rpa/flock-storage.json` | | `FLOCK_RPA_ENABLED` | 否 | 默认 `false`;`true` 才注册 Provider / 消费队列 | | `FLOCK_SELECTOR_PICKUP_DATE` | 启用时是 | 取货日期 | | `FLOCK_SELECTOR_PICKUP_ZIP` | 启用时是 | 取件 ZIP | | `FLOCK_SELECTOR_DELIVERY_ZIP` | 启用时是 | 投递 ZIP | | `FLOCK_SELECTOR_PALLET_COUNT` | 启用时是 | 托盘数 | | `FLOCK_SELECTOR_TOTAL_WEIGHT` | 启用时是 | 总重量 lb | | `FLOCK_SELECTOR_LENGTH` / `_WIDTH` / `_HEIGHT` | 启用时是 | 单托尺寸 | | `FLOCK_SELECTOR_NEXT` | 启用时是 | 「下一个」 | | `FLOCK_SELECTOR_CARD_DIRECT` / `_STANDARD` | 启用时是 | 结果卡根节点 | | `FLOCK_SELECTOR_CARD_PRICE` / `_TRANSIT` | 启用时是 | 卡内价格/时效 | | `FLOCK_SELECTOR_LOGIN_EMAIL` / `_PASSWORD` / `_SUBMIT` | 否 | 登录表单 selector 覆盖 | #### 7.6.5 队列与隔离 | 项 | 规则 | |----|------| | 队列名 | `flock-quote-rpa`(与 `quote-rpa` 分离) | | Worker | 可同进程分 consumer,或独立容器;concurrency 默认 1 | | 缓存 key | `cargo_hash` 须含 `provider=flock` 前缀或独立命名空间,避免与 Mothership 串缓存 | | 开关 | `FLOCK_RPA_ENABLED=false` 时零副作用 | #### 7.6.6 验收 - `npm run probe:flock-rpa`(或等价)连续 3 次 exit 0 - 单测:`FLOCK_LIMITS` 边界(3/21 托、45001 lb、637/103/109 in) - 回归:关闭开关后 Mothership probe / embed-demo 不受影响 --- ## 第 8 章 前端实现规格 ### 8.1 宿主内嵌查价组件 - 由宿主系统加载;地址组件须支持 **Places 联想点选**(见 PRD §4.2.1 方案 A) - 表单字段:`pallet_count`(托盘数)、单托 weight/dims(支持 kg/cm 展示,提交前换算) - 结果区:四档 Tab(standard/guaranteed × lowest/fastest) - 状态机见 [查价系统-UI设计.md](./查价系统-UI设计.md) §2.4(待 UI 文档同步 v1.1) ### 8.2 管理端(本期唯一登录入口) | 路由 | 功能 | |------|------| | `/admin/alerts` | 预警列表、标记已处理 | | `/admin/rpa` | RPA Worker 状态、队列、成功率(开发进度) | | `/admin/dashboard` | 汇总指标 | | `/admin/queues` | Bull Board 队列监控(P1,开发/运维) | > **移除** `/admin/markup` 运营登录页;加价由宿主 `PUT /api/markup-configs` 配置。 ### 8.3 技术栈 - UI:Tailwind CSS + shadcn/ui - 表单:react-hook-form + zod - 请求:fetch + 自定义 hook `useQuotePolling` --- ## 第 9 章 MVP 实现路径 ### 9.1 Week 1:基础链路 | 序号 | 任务 | 产出 | 验收 TC | |------|------|------|---------| | 1 | docker-compose + Prisma migrate | 5 表 + sys_user | - | | 2 | POST/GET /api/quotes 骨架 | API 通 | TC-101 | | 3 | validation + cargo_hash | 入参校验 | TC-201~204 | | 4 | Redis L1/L2/L3 + 击穿锁 | 缓存模块 | TC-104, TC-105 | | 5 | idempotency_record | 幂等 | TC-207, TC-404 | ### 9.2 Week 2:RPA + 加价 | 序号 | 任务 | 产出 | 验收 TC | |------|------|------|---------| | 6 | **headed 录制 + env** | `record-mothership.sh` → `MOTHERSHIP_QUOTE_URLS` + `RPA_SELECTOR_*` | - | | 7 | QuotePageAdapter + session-manager storageState | 多 URL 探测 + 匿名 Cookie | TC-601 probe 1 次 | | 8 | BullMQ + rpa-worker | 异步询价 | TC-601 | | 9 | MothershipRPAProvider | 四档 + 地址 fill-verify | TC-601 probe 3/3 | | 10 | Fallback + 熔断 | 降级 | TC-205, TC-209 | | 11 | PricingEngine + markup API | 加价 | TC-103, TC-505 | | 12 | 验证码/STRUCT_CHANGE 处理 | 异常 | TC-603, TC-604 | ### 9.3 Week 3:预警 + 前端 + 上线 | 序号 | 任务 | 产出 | 验收 TC | |------|------|------|---------| | 11 | AlertService + 偏差检测 | 预警 | TC-210~212 | | 12 | 客户查价页 + 管理端 | UI | TC-101 UI | | 13 | 限流 + RBAC | 安全 | TC-501~506 | | 14 | 全量 TC + Go/No-Go | 上线 | PRD §12.7 | ### 9.4 MVP 不做的功能 - 多币种 - CC API 对接 - Priority1 Provider - **Flock Freight Provider(口径见 §7.6;开关默认关闭)** - 嵌入私仓预报 - 邮件/企微通知(仅站内预警) --- ## 第 10 章 风险与工程约束 | 风险 | 影响 | 缓解 | |------|------|------| | RPA 被 Mothership 反爬 | RPA 成功率 <85% | L3 stale + 人工兜底;熔断 | | Playwright 内存占用 | Worker OOM | `shm_size: 1gb`;并发=2 | | Redis 单点 | 缓存/队列不可用 | 生产启用 AOF 持久化 | | 非程序员维护 | 部署失败 | 仅暴露 `docker compose` 三条命令 | | 3 周工期紧 | 功能砍减 | 严格按 §9.4 不扩展 | --- ## 第 11 章 测试与 CI ### 11.1 测试分层 | 层级 | 范围 | 工具 | |------|------|------| | Unit | pricing、validation、cargo_hash | Vitest | | Integration | API + Prisma + Redis | Vitest + Testcontainers | | E2E | 查价页轮询(RPA mock) | Playwright | | Load | 60 QPS 限流 | k6 | ### 11.2 CI 门禁(GitHub Actions 示意) ```yaml # .github/workflows/ci.yml - npm run lint - npm run test:unit - npm run test:integration - npm run build ``` 上线前手动:PRD §12.7 Go/No-Go 指标。 --- ## 第 12 章 Cursor 开发指引 ### 12.1 推荐开发顺序 1. `prisma/schema.prisma` → migrate 2. `modules/quote/validation.ts` + `types.ts` 3. `modules/cache/redis-cache.ts` 4. `modules/quote/orchestrator.ts` 5. `app/api/quotes/route.ts` 6. **`scripts/record-mothership.sh` → `.env` RPA 变量**(GD-15 前置) 7. `workers/rpa/quote-page-adapter.ts` + `session-manager.ts` 8. `workers/rpa/mothership.ts` 9. `scripts/probe-rpa.ts`(TC-601) 10. 宿主内嵌查价组件 + 管理端 ### 12.2 Cursor Rules 建议(`.cursor/rules/chajia.mdc`) ``` - 严格遵循 docs/查价系统-PRD.md v0.6 与 docs/查价系统-技术设计.md v1.2 - 金额使用 DECIMAL,禁止 float 计算 - 缓存优先级 L1 > L2 > L3,service_level 不入 cargo_hash - 一次 RPA 返回四档(standard/guaranteed × lowest/fastest),禁止多次 RPA - 地址必须联想点选;cargo_hash 含 place_id - MOTHERSHIP_QUOTE_URLS 必填;禁止 dashboard / /quote 默认 URL - login 无凭据 → STRUCT_CHANGE,禁止误报 SESSION_EXPIRED - pallet_count 替代 quantity - API 统一响应包络 { code, message, data } - 所有 async 必须 catch 并 log,禁止空 catch ``` ### 12.3 单任务拆分示例(可直接建 Issue) | Issue | 描述 | 文件 | |-------|------|------| | CHJ-001 | Prisma schema + seed | `prisma/` | | CHJ-002 | Redis 三层缓存 | `modules/cache/` | | CHJ-003 | QuoteOrchestrator | `modules/quote/` | | CHJ-004 | POST/GET quotes API | `app/api/quotes/` | | CHJ-005 | QuotePageAdapter + RPA Worker | `workers/rpa/` | | CHJ-005b | probe-rpa 探针 | `scripts/probe-rpa.ts` | | CHJ-006 | 查价前端页 | `app/page.tsx` | | CHJ-007 | 加价管理端 | `app/admin/markup/` | | CHJ-008 | 预警中心 | `app/admin/alerts/` | --- ## 附录 A:与 PRD 章节映射 | PRD 章节 | 技术设计章节 | |----------|--------------| | 工程收敛决策 GD-1~15 | §1、§3.7、§4.5、§7 | | 第 8 章 技术架构 / §8.2 RPA | §7.2~§7.5 | | 第 9 章 数据模型 | §3 | | 第 8.5 章 API | §4 | | 第 11 章 异常场景 | §2.4、§7.3 | | 第 12 章 验收标准 | §9、§11 | | 第 13 章 交付计划 | §9 | | 第 14 章 Flock Freight(GD-16) | §7.6 | ## 附录 B:货物类型枚举(GD-8) `general_freight` | `machinery` | `furniture` | `electronics` | `building_materials` | `auto_parts` | `food_nonperishable` | `apparel` | `other` ## 附录 C:种子数据(seed.ts) | 用户 | 角色 | 用途 | |------|------|------| | admin_demo | admin | 管理端:预警/RPA/开发进度 | 默认密码:`Demo@123`(仅开发环境) > 查价联调使用宿主模拟 `customer_id` + Service Token,**不创建** customer_demo / operator_demo。 ## 附录 D:v1.1 → v1.2 变更追踪(对齐 PRD v0.6) | ID | 章节 | Before (v1.1) | After (v1.2) | |----|------|---------------|--------------| | CHG-T01 | 版本 | v1.1 | v1.2 | | CHG-T02 | §5 目录 | 无 adapter/probe | 新增 `quote-page-adapter.ts`、`probe-rpa.ts`、`record-mothership.sh` | | CHG-T03 | §6.2 env | 仅 EMAIL/PASSWORD | `MOTHERSHIP_QUOTE_URLS`、`RPA_STORAGE_STATE_PATH`、`RPA_SELECTOR_*`、`RPA_USE_PATCHRIGHT` | | CHG-T04 | §6.1 compose | 无 RPA env/volume | rpa-worker 注入 RPA env + `rpa_state` volume | | CHG-T05 | §7.2 | 单 URL preCheck 隐含 dashboard | QuotePageAdapter 多 URL;禁止 dashboard 默认 | | CHG-T06 | §7.2 | 无 adapter 接口 | 新增 §7.2.3 QuotePageAdapter + selector env 表 | | CHG-T07 | §7.2.1 | 硬编码 data-testid | 全部改为 `RPA_SELECTOR_*` env | | CHG-T08 | §7.3 | SESSION_EXPIRED 含 login 无凭据 | login 无凭据→STRUCT_CHANGE;新增 QUOTE_ENTRY_UNAVAILABLE | | CHG-T09 | §7.5 | 无 | storageState 生命周期 | | CHG-T10 | §8.2 | 无队列页 | `/admin/queues` Bull Board(P1) | | CHG-T11 | §9.2 | RPA 直接开发 | Week2 前置 headed 录制 → adapter → probe 3/3 | | CHG-T12 | §12.1/12.2 | mothership 先于 env | 录制 env 先于 RPA 实现 | ## 附录 E:v1.2 → v1.3 变更追踪(对齐 PRD v0.7) | ID | 章节 | Before (v1.2) | After (v1.3) | |----|------|---------------|--------------| | CHG-T13 | 版本 | v1.2 | v1.3 | | CHG-T14 | §5 目录 | 仅 mothership provider | 增 `flock-freight*`、`flock-limits`、`probe-flock` / `record-flock` | | CHG-T15 | §7.6 | 无 | 新增 FlockFreightRPAProvider:硬限、步骤、env、队列隔离 | | CHG-T16 | §9.4 | 未列 Flock | MVP 明确不做 Flock 实现(文档先行) |