# implementation-plan

> 功能实施步骤与执行日志。**v1.1 基线（Phase 0~9）已完成**；**v1.2 增量从 Phase 0 的 task-119 起按序执行**。
> 任务详情见 `docs/查价系统-工程任务拆解-v1.2.md`（当前权威版本）。

## 里程碑

| 里程碑 | 条件 | 验收 |
|--------|------|------|
| M1 | Phase 0~4 基线 | 已完成 — `POST /quotes` L2 命中、幂等、校验 400 |
| M2-v1.2 | Phase 0(119–120) + Phase 5.0~5 + Phase 10 | `RPA_MOCK_MODE=false` 下 `probe-rpa.ts` **连续 3 次 exit 0**，四档 `source_type=rpa` |
| M3 | Phase 8 基线 | 已完成 — 宿主内嵌组件 + 管理端 |
| M4 | Phase 9 基线 + task-133~135 | CI 全绿 + Go/No-Go probe 门禁 |

## v1.2 当前执行入口（Phase 0 增量）

| 顺序 | Task | 状态 | 说明 |
|------|------|------|------|
| 1 | task-119 | **Done** | GD-15 RPA env 模板 + fail-fast |
| 2 | task-120 | **Done** | docker `rpa_state` volume |
| 3 | task-121~122 | 待执行 | RPA 录制前置 |
| 4 | task-123~131 | **Done** | RPA v1.2 重构 |
| 5 | task-132 | 已完成 | Bull Board |
| 6 | task-133~135 | 待执行 | TC-601 + CI probe |

> Phase 0 基线 072→073→070→071→097→030 与 119→120 均已完成；**下一入口 task-121**。

---

## Phase 0 — 环境与底座

> Task：072 → 073 → **119 → 120** → 070 → 071 → 097 → 030  
> 状态：**072–030、119–120 已完成**；**121 起待执行**

### Step 0.1 Docker Compose 四容器

- **Task**：task-072
- **输出**：`docker-compose.yml`（mysql/redis/next-app/rpa-worker + healthcheck）
- **验证**：`docker compose up -d` 四容器 healthy
- **状态**：Done
- 执行记录 1：创建 docker-compose.yml，四服务 + mysql/redis healthcheck + rpa-worker shm_size 1gb
- 执行记录 2：本机 Docker Desktop 未运行，容器启动待用户本地验证

### Step 0.2 环境变量与 Dockerfile

- **Task**：task-073
- **输出**：`.env.example` / `Dockerfile` / `Dockerfile.worker`
- **验证**：`docker compose build` 无报错
- **状态**：Done
- 执行记录 1：创建 .env.example（6 项必填变量）、Next standalone Dockerfile、worker Dockerfile
- 执行记录 2：`npm run build` 通过；docker build 待 Docker 启动后验证

### Step 0.3 v0.6 RPA 环境变量模板（v1.2 增量）

- **Task**：task-119
- **输入**：PRD §8.2.1 GD-15；技术设计 §6.2 CHG-T03
- **输出**：`.env.example` 增 `MOTHERSHIP_QUOTE_URLS`、`RPA_STORAGE_STATE_PATH`、`RPA_SELECTOR_*`（8 项）、`RPA_USE_PATCHRIGHT`
- **验证**：含全部 RPA 必填项；缺 `MOTHERSHIP_QUOTE_URLS` 时 rpa-worker fail-fast 退出
- **约束**：禁止默认 dashboard / `/quote` URL
- **状态**：Done
- 执行记录 1：lib/rpa/env.ts + assertRpaWorkerEnv；.env.example v0.6 模板；移除 dashboard 默认 URL
- 执行记录 2：__tests__/lib/rpa/env.test.ts 6 用例通过；缺 URL 时 worker exit 1

### Step 0.4 Docker RPA 持久化（v1.2 增量）

- **Task**：task-120
- **输入**：技术设计 §6.1 §7.5 CHG-T04
- **输出**：`docker-compose.yml` rpa-worker 注入 RPA env + `rpa_state` volume → `/app/.rpa`
- **验证**：`docker compose up -d` 后 `.rpa/mothership-storage.json` 跨容器重启保留
- **状态**：Done
- 执行记录 1：rpa-worker 透传 MOTHERSHIP_QUOTE_URLS/RPA_* env；rpa_state volume 挂载 /app/.rpa
- 执行记录 2：docker compose config 校验通过；.gitignore 忽略 .rpa/

### Step 0.5 Prisma Schema

- **Task**：task-070
- **输出**：`prisma/schema.prisma`（7 实体）
- **验证**：`npx prisma validate` 零错误
- **状态**：Done
- 执行记录 1：7 模型 SysUser/QuoteRecord/IdempotencyRecord/QuoteCacheMeta/MarkupConfig/AlertLog/AuditLog
- 执行记录 2：`npx prisma validate` 通过；`npx prisma generate` 成功

### Step 0.6 Migrate + Seed

- **Task**：task-071
- **输出**：`prisma/migrations/` + `prisma/seed.ts`（admin_demo）
- **验证**：migrate 成功；sys_user 有 1 行 admin
- **状态**：Done
- 执行记录 1：migration `20260616000000_init` + markup CHECK 约束；seed admin_demo/Demo@123
- 执行记录 2：migrate deploy + seed 已通过（MySQL 宿主机端口改为 3307 避开本机 3306 冲突）

### Step 0.7 Prisma 单例

- **Task**：task-097
- **输出**：`lib/prisma.ts`
- **验证**：`prisma.$connect()` 成功
- **状态**：Done
- 执行记录 1：lib/prisma.ts 全局单例，dev 热重载防多实例
- 执行记录 2：tsc 编译通过；$connect 待 MySQL 就绪后验证

### Step 0.8 Redis 单例

- **Task**：task-030
- **输出**：`lib/redis.ts`
- **验证**：`getRedis().ping()` → PONG
- **状态**：Done
- 执行记录 1：lib/redis.ts getRedis() 单例 + REDIS_URL 缺失抛错 + error 日志
- 执行记录 2：tsc 编译通过；ping 待 Redis 容器启动后验证

---

## Phase 1 — 基础设施与缓存

> Task：096 → 031 → 032 → 033 → 034 → 035 → 035-ip → 036

### Step 1.1 统一响应包络

- **Task**：task-096
- **输出**：`lib/response.ts`（ok/fail）
- **验证**：Vitest 覆盖 code/message/data
- **状态**：Done
- 执行记录 1：lib/response.ts 实现 ok/fail + Cache-Control: no-store
- 执行记录 2：__tests__/lib/response.test.ts 2 用例通过

### Step 1.2 L1 幂等缓存

- **Task**：task-031
- **输出**：`modules/cache/redis-cache.ts` getL1/setL1
- **验证**：SETNX + TTL 86400s
- **状态**：Done
- 执行记录 1：setL1 SETNX 不覆盖；getL1 JSON 反序列化
- 执行记录 2：单测验证 TTL=86400

### Step 1.3 L2 热缓存

- **Task**：task-032
- **输出**：getL2/setL2
- **验证**：TTL 180~210s jitter
- **状态**：Done
- 执行记录 1：setL2 写入 quote:{cargo_hash}，TTL 180+random(0~30)
- 执行记录 2：单测 20 次采样 TTL 均在 180~210 区间

### Step 1.4 L3 Stale 缓存

- **Task**：task-033
- **输出**：getL3/setL3
- **验证**：TTL 1800s
- **状态**：Done
- 执行记录 1：setL3 写入 stale:{cargo_hash}，TTL 1800s
- 执行记录 2：单测验证 TTL=1800

### Step 1.5 缓存击穿锁

- **Task**：task-034
- **输出**：acquireLock/releaseLock/waitAndGetL2
- **验证**：SETNX lock:quote:{hash} TTL 5s
- **状态**：Done
- 执行记录 1：acquireLock SETNX + releaseLock DEL + waitAndGetL2 2s
- 执行记录 2：单测验证重复 acquire 返回 false

### Step 1.6 客户限流

- **Task**：task-035
- **输出**：`modules/cache/rate-limiter.ts`
- **验证**：60 次/分钟/customer
- **状态**：Done
- 执行记录 1：checkRateLimit INCR+EXPIRE 60s，超限 RATE_LIMITED
- 执行记录 2：单测 60 通过 + 第 61 拒绝

### Step 1.7 IP 限流

- **Task**：task-035-ip
- **输出**：`modules/cache/ip-rate-limiter.ts`
- **验证**：1000 次/分钟/IP，超限封禁 10 分钟
- **状态**：Done
- 执行记录 1：checkIpRateLimit ban:ip:{ip} + ratelimit:ip:{ip}
- 执行记录 2：单测 1000 通过 + 超限 banned=true

### Step 1.8 RPA 熔断

- **Task**：task-036
- **输出**：`modules/cache/circuit-breaker.ts`
- **验证**：连续 3 次失败 open，TTL 600s
- **状态**：Done
- 执行记录 1：isCircuitOpen/openCircuit/closeCircuit/recordRpaFailure
- 执行记录 2：单测 3 次失败熔断 + closeCircuit 清除

---

## Phase 2 — 鉴权

> Task：001 → 002 → 003 → 004 → 005 → 098

### Step 2.1 JWT 签发/校验

- **Task**：task-001 ~ task-003
- **输出**：`modules/auth/`（宿主 token + 管理员 JWT）
- **验证**：有效/过期/伪造 token 分支
- **状态**：Done
- 执行记录 1：jwt.ts sign/verify/decode + login route + middleware 路由保护
- 执行记录 2：jwt.test.ts 4 用例通过；tsc 零错误

### Step 2.2 RBAC 中间件

- **Task**：task-004, task-005
- **输出**：`middleware.ts` 鉴权 + 权限校验
- **验证**：越权 403；权限 `alert:read` `rpa:operate` `pricing:markup:write`
- **状态**：Done
- 执行记录 1：service-token.ts 租户绑定 + rbac.ts requirePermission
- 执行记录 2：service-token/rbac 单测 7 用例通过

### Step 2.3 限流

- **Task**：task-098
- **输出**：令牌桶限流
- **验证**：超限返回 RATE_LIMITED
- **状态**：Done
- 执行记录 1：lib/gateway-retry.ts 5xx 单次重试 → 503 INTERNAL_ERROR
- 执行记录 2：gateway-retry.test.ts 3 用例通过

---

## Phase 3 — 领域逻辑

> Task：040~044 → 010~014, 092, 095, 109, 111, 019

### Step 3.1 加价引擎

- **Task**：task-040, task-041, task-044
- **输出**：`modules/pricing/engine.ts`
- **验证**：0~30% 边界；仅 raw_freight 加价
- **状态**：Done
- 执行记录 1：lib/math.ts roundHalfUp + engine applyMarkup/getMarkupPercent/applyMarkupToQuotes
- 执行记录 2：math.test.ts + engine.test.ts 11 用例通过；tsc 零错误

### Step 3.2 入参校验

- **Task**：task-010, task-011
- **输出**：`modules/quote/validation.ts`
- **验证**：Mothership 官方限制；地址 place_id
- **状态**：Done
- 执行记录 1：validation.ts MOTHERSHIP_LIMITS + zod 校验 + unit-converter + cargo-hash
- 执行记录 2：validation/unit-converter/cargo-hash 单测 TC-201~204 通过

### Step 3.3 幂等模块

- **Task**：task-012
- **输出**：`modules/quote/idempotency.ts`
- **验证**：24h 同 request_id 返回同一 quote_id
- **状态**：Done
- 执行记录 1：idempotency.ts Redis L1 + DB 双保险 + P2002 冲突处理
- 执行记录 2：idempotency.test.ts 5 用例通过

### Step 3.4 询价编排器

- **Task**：task-013, task-014, task-019
- **输出**：`modules/quote/orchestrator.ts`
- **验证**：L1→L2→RPA 状态机完整
- **状态**：Done
- 执行记录 1：orchestrator submitQuote + quote-id 生成 + rpa-queue 占位入队
- 执行记录 2：orchestrator.test.ts TC-104/105 + processing 路径 3 用例通过

### Step 3.5 Fallback 编排

- **Task**：task-092
- **输出**：`modules/quote/fallback-orchestrator.ts`
- **验证**：RPA 失败 → L3 → STALE_FALLBACK 预警
- **状态**：Done
- 执行记录 1：handleRpaFailure L3 降级/失败分支 + alert_log 写入
- 执行记录 2：fallback-orchestrator.test.ts TC-205/206 通过

### Step 3.6 置信度与四档结构

- **Task**：task-095, task-109, task-111
- **输出**：confidence_score + 4 档 quotes_json 结构
- **验证**：TC-102 四档验收
- **状态**：Done
- 执行记录 1：confidence.ts + quote-completeness assertFourTiers + quote-id P2002 audit
- 执行记录 2：confidence/quote-completeness/quote-id 单测 13 用例通过；npm test 74/74

---

## Phase 4 — API 层（里程碑 M1）

> Task：015 → 016 → 017 → 042 → 043 → 018

### Step 4.1 POST /api/quotes

- **Task**：task-015
- **输出**：`app/api/quotes/route.ts`
- **验证**：TC-104 cache 命中、TC-105 幂等、TC-201~204 校验 400
- **状态**：Done
- 执行记录 1：route.ts 薄层：鉴权 + 限流 + orchestrator + ValidationError→400
- 执行记录 2：quotes-routes.test.ts 2 用例通过；npm test 91/91

### Step 4.2 GET /api/quotes/{quote_id}

- **Task**：task-016
- **输出**：`app/api/quotes/[quote_id]/route.ts`
- **验证**：轮询 processing→done；404 QUOTE_NOT_FOUND
- **状态**：Done
- 执行记录 1：quote-serializer.ts expired/confidence 格式化 + 越权 403
- 执行记录 2：quote-serializer + quotes-routes 单测 TC-501/404 通过

### Step 4.3 GET /api/quotes/history

- **Task**：task-017
- **输出**：`app/api/quotes/history/route.ts`
- **验证**：分页；customer_id 隔离
- **状态**：Done
- 执行记录 1：history/route.ts 强制 page+size + customer_id 过滤
- 执行记录 2：TC-506 缺分页→400 单测通过

### Step 4.4 PUT /api/markup-configs/{customer_id}

- **Task**：task-042, task-043
- **输出**：`app/api/markup-configs/[customer_id]/route.ts`
- **验证**：权限 + 0~30% 校验
- **状态**：Done
- 执行记录 1：GET list + PUT upsert；RBAC pricing:markup:write + 0~30 校验
- 执行记录 2：TC-502/505 单测通过；isKnownCustomer 客户存在校验

### Step 4.5 询价超时处理

- **Task**：task-018
- **输出**：processing 30s 超时 → QUOTE_TIMEOUT
- **验证**：TC 超时场景
- **状态**：Done
- 执行记录 1：timeout-sweeper.ts 10s 扫描；L3→stale / 无L3→QUOTE_TIMEOUT
- 执行记录 2：timeout-sweeper.test.ts 4 用例通过；worker:scheduler 脚本已加

---

## Phase 5 — RPA

> Task：020~029 → 110 → 093, 099, 100

### Step 5.1 BullMQ 队列

- **Task**：task-020 ~ task-022
- **输出**：`workers/rpa/queue.ts` + job 定义
- **验证**：入队/消费/重试策略
- **状态**：Done
- 执行记录 1：QuoteProvider 接口 + BullMQ quote-rpa 队列 + session-manager 登录态
- 执行记录 2：rpa-queue 入队 + worker index 消费/熔断暂停；103 单测通过

### Step 5.2 Playwright RPA Provider

- **Task**：task-023 ~ task-028
- **输出**：`workers/rpa/mothership-provider.ts`
- **验证**：地址联想点选 + 四档抓取
- **状态**：Done
- 执行记录 1：mothership.ts preCheck/fillAddress/fillCargo/scrapeQuotes/validateResult + error-mapper
- 执行记录 2：validateResult + mock 四档单测通过；RPA_MOCK_MODE 可本地联调

### Step 5.3 RPA 管理 API

- **Task**：task-029, task-093, task-099, task-100, task-110
- **输出**：Worker 状态/暂停/恢复 API
- **验证**：RPA_CAPTCHA 暂停 Worker
- **状态**：Done
- 执行记录 1：job-handler 成功写 L2/L3/落库/偏差检测；失败走 fallback；管理 API 三路由
- 执行记录 2：GET /api/rpa/status + pause + manual-fallback；middleware 管理员路由已放行

---

## Phase 6 — 预警 / 审计

> Task：050~057 → 094 → 105

### Step 6.1 预警服务

- **Task**：task-050 ~ task-055
- **输出**：`modules/alert/service.ts`
- **验证**：4 种预警类型写入 alert_log
- **状态**：Done
- 执行记录 1：writeAlert 统一写入 + deviation/fallback/job-handler/sweeper 重构
- 执行记录 2：8 类预警（含 STRUCT_CHANGE/SESSION_EXPIRED/MANUAL_FALLBACK）；114 单测通过

### Step 6.2 预警 API

- **Task**：task-056, task-057
- **输出**：`app/api/alerts/` 列表 + resolve
- **验证**：管理员 RBAC；分页
- **状态**：Done
- 执行记录 1：GET /api/alerts 分页+type/status 筛选；POST resolve 更新 resolved
- 执行记录 2：alerts-routes.test.ts 6 用例通过；middleware 已放行 /api/alerts

### Step 6.3 审计日志

- **Task**：task-094, task-105
- **输出**：`modules/audit/` + AuditLog 写入
- **验证**：高危操作可追溯
- **状态**：Done
- 执行记录 1：writeAudit + recordSecurityEvent；越权/注入/IP封禁写 SECURITY
- 执行记录 2：audit/service.test.ts 3 用例；quote-id/manual-fallback 已迁移

---

## Phase 7 — 观测指标

> Task：101 → 102

### Step 7.1 三率指标

- **Task**：task-101, task-102
- **输出**：API/RPA/realtime rate 采集
- **验证**：GD-4 指标可查询
- **状态**：Done
- 执行记录 1：modules/metrics/collector.ts Redis 计数器 + orchestrator/RPA/fallback/sweeper 埋点；GET /api/metrics/dashboard
- 执行记录 2：collector 7 用例 + dashboard 2 用例通过；npm test 123/123；middleware 已放行 /api/metrics

---

## Phase 8 — 前端

> Task：060~069 → 106 → 063, 064, 079, 103, 104, 080

### Step 8.1 宿主内嵌查价组件

- **Task**：task-106（替代 075~077）
- **输出**：内嵌组件（地址联想、托盘数、四档展示、3min 倒计时）
- **验证**：异步轮询；L2 命中无 RPA
- **状态**：Done
- 执行记录 1：Tailwind+Geist+UI 组件库；QuoteForm/QuoteCard/CountdownTimer/QuoteResultPanel；use-quote-polling 退避轮询；EmbeddedQuoteWidget + /embed-demo
- 执行记录 2：pollQuoteUntilDone 3 用例通过；npm test 126/126；next build 20 路由全绿

### Step 8.2 管理端

- **Task**：task-060 ~ task-069, task-063, task-064, task-079, task-103, task-104, task-080
- **输出**：`app/admin/` 预警中心 + RPA/开发进度
- **验证**：管理员登录；预警处理；文案全中文
- **状态**：Done
- 执行记录 1：/login /dashboard /admin/alerts /admin/rpa /admin/dashboard；AdminLayout+JWT 本地存储；403/not-found 错误页
- 执行记录 2：预警列表分页+标记已处理+RPA 暂停入口；指标看板 GD-4 四卡；仅 admin 入口无 customer 卡片

---

## Phase 9 — 测试 / CI

> Task：081~118 → 091

### Step 9.1 单元测试

- **Task**：task-081 ~ task-091
- **输出**：Vitest 覆盖核心模块
- **验证**：覆盖率达标
- **状态**：Done
- 执行记录 1：task-081~087 单测已覆盖 engine/validation/cargo-hash/idempotency/deviation；补 surcharge=0；test:unit 脚本 104 用例
- 执行记录 2：npm run test:unit 全绿；engine TC-103 markup=32/367 精确到分

### Step 9.2 集成 / E2E

- **Task**：task-112 ~ task-118
- **输出**：API 集成测试 + Playwright E2E
- **验证**：TC 全集通过
- **状态**：Done
- 执行记录 1：新增 quotes.integration/security/l2-expire/cache-consistency/rpa-admin/recalc-from-db；load-tests 三脚本；go-no-go-check.ts
- 执行记录 2：test:integration 33 + E2E 7 全绿；go-no-go --mock PASS

### Step 9.3 CI 流水线

- **Task**：task-091
- **输出**：`.github/workflows/` 或等效 CI
- **验证**：lint + test + build 全绿
- **状态**：Done
- 执行记录 1：ci.yml（lint+test:unit+test:integration+build）；e2e-nightly.yml（E2E+k6+go-no-go）
- 执行记录 2：npm test 145/145；lint 零错误；playwright 7/7；build 20 路由全绿

---

## Dev Infra — Docker 绕过 / WSL 原生

### Step Dev.1 本地开发基础设施修复

- **输出**：`scripts/dev/wsl-infra.sh`、`bootstrap-wsl-infra.ps1`、`diagnose-docker.ps1`；`DEV_INFRA_MODE=native`
- **验证**：`npm run dev:infra:setup` + `npm run dev:start` 全绿
- **状态**：Done
- 执行记录 1：根因 Docker Desktop WSL IPC 死锁(engine_500)；移除有害 SwitchDaemon；WSL Ubuntu-22.04 原生 mysql/redis；动态解析 WSL IP 供 Windows 连接
- 执行记录 2：`dev:start` migrate/seed/smoke/next:3000 全绿；`dev:diagnose-docker` 确认 engine_500 并给出 WSL 路径

### Step Dev.2 真实 RPA 验收（关闭 Mock）

- **输出**：PRD v0.5 匿名查价条款；`.env` `RPA_MOCK_MODE=false`；`scripts/smoke-real-rpa.ts`
- **验证**：`npm run smoke:rpa` 返回 4 档 `source_type=rpa`
- **状态**：Done
- 执行记录 1：PRD §8.2 + 技术设计 §7.2 补充匿名查价；关闭 Mock；修复 session-manager 先判登录页再要凭据
- 执行记录 2：实测 `www.mothership.com/quote` 404；默认 URL 改 `dashboard.mothership.com`；真实冒烟需配置 MOTHERSHIP 凭据

---

## Phase 10 — RPA v1.2（PRD v0.6 / 技术设计 v1.2）

> Task：119 → 120 → 121 → 122 → 123~131 → 132 → 133~135

### Step 10.0 环境与 Docker RPA 持久化

- **Task**：task-119, task-120（**与 Phase 0 Step 0.3–0.4 同任务**）
- **输出**：`.env.example` RPA env；`docker-compose.yml` rpa_state volume
- **验证**：缺 `MOTHERSHIP_QUOTE_URLS` fail-fast；storageState 跨重启保留
- **状态**：Done（见 Phase 0 Step 0.3–0.4）

### Step 10.1 RPA 录制前置

- **Task**：task-121, task-122
- **输出**：`scripts/record-mothership.sh`；`docs/rpa-recording-sop.md`
- **验证**：headed 录制产出 URL + 8 个 selector env
- **状态**：Done
- 执行记录 1：新增 `record-mothership.sh`（codegen headed、GD-15 URL 拦截、storageState 读写）及 `docs/rpa-recording-sop.md`（LA→Dallas 样例 + 8 selector 映射表 + checklist）
- 执行记录 2：`vitest __tests__/scripts/record-mothership.test.ts` 3/3 通过

### Step 10.2 RPA v1.2 重构

- **Task**：task-123 ~ task-131
- **输出**：`quote-page-adapter.ts`；storageState 生命周期；selector env 化；错误分类 v0.6；probe 3/3
- **验证**：`bash scripts/probe-rpa-3x.sh` 连续 3 次 exit 0
- **状态**：Done
- 执行记录 1：QuotePageAdapter 多 URL 探测 + storageState + selector env 化 + QUOTE_ENTRY_UNAVAILABLE + probe 3x 脚本
- 执行记录 2：单测 adapter/storage/fallback-entry 通过；`RPA_MOCK_MODE=true` probe 3/3 exit 0

### Step 10.3 Bull Board 队列页

- **Task**：task-132
- **输出**：`app/admin/queues/page.tsx`
- **验证**：admin 可见 quote-rpa 队列；非 admin 403
- **状态**：Done
- 执行记录 1：Hono + Bull Board 挂载 `/api/admin/queues`；stats API + 中文摘要卡片；iframe cookie 鉴权；导航入口
- 执行记录 2：`admin-queues.test.ts` 4/4 通过；`npm run build` 含 `/admin/queues` 路由

### Step 10.4 验收与 CI probe 门禁

- **Task**：task-133, task-134, task-135
- **输出**：TC-601 修订；go-no-go probe 3/3；e2e-nightly probe 步骤
- **验证**：Go-No-Go 含 probe 门禁；TC-601 items=4
- **状态**：Done
- 执行记录 1：`lib/rpa/probe-validation.ts` + `run-probe.ts`；`rpa-probe.test.ts`；e2e TC-601 items=4 断言；go-no-go probe 3/3
- 执行记录 2：`e2e-nightly` 增 probe job（secrets 缺失跳过）；`go-no-go --mock` PASS；integration 5 用例通过

### Step 10.5 本地 RPA 环境未配置导致查价超时

- **根因**：`.env` 缺 `MOTHERSHIP_QUOTE_URLS` + 8 项 `RPA_SELECTOR_*` → worker fail-fast 未消费队列 → 前端 30s 轮询超时
- **输出**：`scripts/dev/lib.ps1` `Assert-DevWorkerHealthy`；`start.ps1` 启动后校验 worker 存活
- **验证**：缺 env 时 `npm run dev:start` 明确报错而非静默超时
- **状态**：Done
- 执行记录 1：日志证实 `QTE_20260617_0004` 入队后 worker 因 `MOTHERSHIP_QUOTE_URLS` 空而启动失败
- 执行记录 2：`dev:start` 增 worker 存活/日志 fail-fast 检测，避免「查询超时」误导

### Step 10.6 Windows 录制脚本（无需 bash）

- **输出**：`scripts/record-mothership.ts` + `record-mothership.ps1`；`npm run record:mothership`
- **验证**：`vitest record-mothership.test.ts` 通过；`--help` 可执行
- **状态**：Done
- 执行记录 1：根因 Windows 无 `/bin/bash`；新增 tsx 跨平台录制入口
- 执行记录 2：SOP 补充 Windows 命令；保留 `.sh` 供 Linux/macOS

### Step 10.7 MotherShip 地址歧义：先选后报价

- **需求**：提货/派送在 MotherShip 有多精确坐标时禁止 RPA 自动择一；先返回候选，用户确认后再询价
- **输出**：`POST /api/addresses/mothership-candidates`；`AddressDisambiguationModal`；询价校验 `selected_from_mothership`
- **验证**：Dallas 75201 返回 4 候选；未确认 MotherShip 地址时 `POST /api/quotes` 拒绝
- **状态**：Done
- 执行记录 1：新增候选 API + mock 目录；RPA `fillAddressField` 仅点用户 `mothership_display_label`
- 执行记录 2：`embedded-quote-widget` 多选弹窗；`validation`/`cargo-hash` 测试通过

### Step 10.8 配置 .env + 真实 RPA 地址列表抓取

- **需求**：`.env` 按录制 `112133` 填齐；`RPA_MOCK_MODE=false` 时禁止候选 API 回退 mock catalog
- **输出**：`lib/rpa/locator.ts`；`workers/rpa/address-candidates.ts`；`.env` selector 全量
- **验证**：`mothership-candidates.test.ts` 断言走 `fetchMothershipAddressCandidates`
- **状态**：Done
- 执行记录 1：新增 `pageLocator` 解析 role/text/placeholder；首页 widget 分区 selector
- 执行记录 2：`.env` 写入 `MOTHERSHIP_QUOTE_URLS` + 8 项 selector + 货物 placeholder

### Step 10.9 重录 115059/115541 更新 .env

- **依据**：`mothership-20260617-115059.js`（四档 UI）、`115541.js`（地址分区）
- **变更**：`最低价格` 替代 `最优性价比`；分区改 `text=从何处取货/送货至`；增 `CARGO_SECTION`/`TRANSIT`
- **状态**：Done
- 执行记录 1：`.env` / `.env.example` 按新录制同步
- 执行记录 2：`fillCargo` 支持 `RPA_SELECTOR_CARGO_SECTION` 展开货运详情

### Step 10.10 worker 启动 fail-fast：FASTEST 改可选

- **根因**：`.env` 中 `RPA_SELECTOR_FASTEST_OPTION=` 空；`assertRpaSelectorsEnv` 仍要求 8 项全填
- **修复**：`RPA_REQUIRED_SELECTOR_ENV_KEYS`（7+PRICE）；dev 后台进程预加载 `.env`
- **状态**：Done
- 执行记录 1：`selectors.ts` FASTEST 可空；115059 录制无独立 fastest 档
- 执行记录 2：`lib.ps1` Start-DevBackgroundProcess 注入根 `.env`

### Step 10.11 dev:start 误报 worker 启动失败

- **根因 1**：`infra.env` 覆盖 `.env` 注入；**根因 2**：健康检查读日志 tail 含历史 FASTEST 报错（worker 已启动）
- **修复**：`.env`+`infra.env` 顺序合并；启动前清空 log；仅解析最近一次 run
- **状态**：Done
- 执行记录 1：`Assert-DevWorkerHealthy` 用 `Get-DevLogLatestRunText`
- 执行记录 2：`npm run dev:start` exit 0，worker-rpa pid 存活

### Step 10.12 embed 查价「令牌无效或已过期」

- **根因**：`POST /api/addresses/mothership-candidates` 未纳入 `isHostRoute`，middleware 用 JWT 校验 `demo-host-token`
- **修复**：`middleware.ts` 增加 `pathname.startsWith("/api/addresses")`
- **状态**：Done
- 执行记录 1：点击获取报价先调地址候选 API，非 `/api/quotes` 路径
- 执行记录 2：宿主 Service Token 与 quotes 路由同等鉴权

### Step 10.22 填表 click 假通过 + RPA 超时窗口

- **根因**：`openAddressSide` 吞掉 click 异常；派送误命中不可编辑提货框；30s 窗口不足
- **修复**：`clickStreetInput` 可编辑校验；派送优先 `:focus` + 末位可编辑框；超时 60s
- **状态**：Done
- 执行记录 1：`address-side.ts` 禁止吞 click；`address-suggestions` scroll+超时；pipeline/mothership 阶段日志
- 执行记录 2：`QUOTE_TIMEOUT_MS`/`POLL_TIMEOUT_MS`/`RPA_JOB_TIMEOUT_MS` 对齐 60/60/55s

### Step 10.24 fillAddress 8s 卡死：input:focus 误匹配

- **根因**：`focusedStreetInput` 全页 `:focus` 命中不可点击元素；跳过「先点分区」与录制不一致
- **修复**：widget 内 `getByRole(textbox)`；始终先点分区；click 失败 force 回退
- **状态**：Done
- 执行记录 1：`address-side.ts` 移除 `:focus`；对齐 115541 双点送货至
- 执行记录 2：`address-suggestions` click force 回退；单测通过

### Step 10.23 前端轮询 30s 提前超时

- **根因**：`pollQuoteUntilDone` 固定 15 次 × 2s ≈ 30s，与 `POLL_TIMEOUT_MS=60s` 进度条不一致
- **修复**：轮询改 wall-clock `POLL_TIMEOUT_MS`；embed 超时文案对齐后端
- **状态**：Done
- 执行记录 1：`hooks/use-quote-polling.ts` deadline 驱动；`POLL_MAX_ATTEMPTS` 由常量推导
- 执行记录 2：`use-quote-polling.test.ts` 通过；`quote-result-panel` 文案 60s

### Step 10.21 page.goto 超时 → QUOTE_ENTRY_UNAVAILABLE

- **根因**：`quote-page-adapter` goto 固定 20s；慢网/Cloudflare 下 `domcontentloaded` 未触发
- **修复**：`openQuoteEntryUrl` 45s + commit/load 回退；导航超时与错误文案增强
- **状态**：Done
- 执行记录 1：`RPA_PAGE_GOTO_TIMEOUT_MS=45000`；`quote-page-adapter` 三档 waitUntil
- 执行记录 2：`quote-error-messages` 提示 VPN/headless/storageState

### Step 10.20 询价 30s 超时：派送地址框定位失败

- **根因**：提货/派送共用 `搜索地址` selector；填完提货后仍命中提货框，Playwright 等 30s → `QUOTE_TIMEOUT`
- **修复**：派送取 widget 内最后一个可见 textbox；双击「送货至」；page 默认超时 12s；候选按 display_label 去重
- **状态**：Done
- 执行记录 1：`address-side.ts` delivery 用 `lastVisible` + 双点分区
- 执行记录 2：`session-manager` 降默认超时；`address-candidates` 去重

### Step 10.19 移除货物类型表单项 + 联想键入对齐录制

- **需求**：隐藏「货物类型」；默认 `general_freight`；修复 Dallas 派送联想不出现
- **根因**：`formatAddressQuery` 未带邮编；`Garland` 误解析进 street
- **状态**：Done
- 执行记录 1：`quote-form.tsx` 移除货物类型；`ensureAddressSuggestions` 多策略键入
- 执行记录 2：`formatAddressQuery` 改为 `街道, 城市, 州 邮编`；`normalizeParsedStreet` 修正

### Step 10.29 MotherShip 键入去掉邮编 + 表单移除邮编字段

- **需求**：RPA 联想键入不带邮编；查价表单取消邮编输入
- **修复**：`formatAddressQuery` 仅 `街道, 城市, 州, USA`；`quote-form` 移除邮编栏
- **状态**：Done
- 执行记录 1：`address-suggestions.ts` 键入策略去 zip；候选 API zip 可空
- 执行记录 2：`quote-form.tsx` 移除邮编；zip 仍由 MotherShip 候选/联想结果补全


- **根因**：`mothership.ts` finally 调用 `shouldKeepBrowserOpen`/`captureRpaDebugScreenshot` 但 import 在重构中丢失
- **修复**：唯一实现迁至 `lib/rpa/env.ts`；`mothership.ts` 显式 import
- **状态**：Done
- 执行记录 1：`page-prep.ts` 改为 re-export；无局部 fallback
- 执行记录 2：测试 import 路径对齐

### Step 10.27 evaluate __name helper 污染

- **根因**：tsx/esbuild 向 `page.evaluate(fn)` 注入 `__name` 等 helper，browser context 未定义
- **修复**：`browser-eval-scripts.ts` 纯字符串脚本；全部 evaluate/waitForFunction 改传 string
- **状态**：Done
- 执行记录 1：7 处违规调用点已迁移；禁止 function 引用传入 evaluate
- 执行记录 2：`browser-eval-scripts.test.ts` 无 __name 断言通过

### Step 10.26 cargo hydration：mounted ≠ interactive

- **根因**：React hydration 后 input 已 mounted 但 pointer-events/disabled 未解除；旧 ready 用 class+mounted 误判
- **修复**：`isCargoStateReady` 仅认 interactive 托盘输入；`waitForCargoInteractiveState` = waitForFunction
- **状态**：Done
- 执行记录 1：`CargoStateEvidence` 增 `blockedCargoFields` 对比 mounted vs interactive
- 执行记录 2：`cargo-state.test.ts` 更新；禁止 class/mounted-only ready

### Step 10.25 evaluate boundary：probeCargoOnWidget scope leakage

- **根因**：`widget.evaluate(() => probeCargoOnWidget(...))` 引用 Node 函数，浏览器 context 未定义
- **修复**：probe 逻辑完整内联至 evaluate 回调；`fillMountedCargoField` 已是自包含回调
- **状态**：Done
- 执行记录 1：删除 Node 侧 `probeCargoOnWidget` 外部调用
- 执行记录 2：全 workers/rpa evaluate 审计无其他 Node 函数引用

### Step 10.24 querySelector 空 selector crash

- **根因**：`RPA_SELECTOR_QUOTE_WIDGET?.trim() ?? fallback` 对 env 空串 `""` 无效；未进 registry；evaluate 内 `document.querySelector("")`
- **修复**：`getSelector()` fail-fast + 内置回退；`locator.evaluate(widgetEl)` 替代 evaluate 内拼 selector
- **状态**：Done
- 执行记录 1：`env.ts`/`selectors.ts`/`cargo-state.ts`/`page-prep.ts`/`address-side.ts`/`address-suggestions.ts`
- 执行记录 2：`selectors.test.ts` 空串回退用例通过

### Step 10.23 fillCargo cargo UI state machine

- **根因**：失败截图 cargo 为折叠摘要态；地址填完后仍 delivery_active，未 settle → cargo_trigger 无效；旧逻辑用 placeholder visible 判 ready
- **修复**：`cargo-state.ts` settle/trigger/waitCargoStateReady（active class + window + mounted enabled 组合）
- **状态**：Done
- 执行记录 1：禁止连点 nth(2)；`fillMountedCargoField` 在 state ready 后执行
- 执行记录 2：`cargo-state.test.ts` + `mothership.cargo.test.ts` 通过

### Step 10.22 fillCargo 根因：货运详情点击节点与录制不一致

- **根因**：录制 `page.locator('div').filter({hasText:'货运详情'}).nth(4)`；代码用 `widget>>filter>>nth=4` 或 `getByText().first()` → 点错节点，托盘 placeholder 永不出现
- **修复**：`clickRecordingCargoSection` + `page.getByPlaceholder` 主路径；以 placeholder 可见性验证展开成功
- **状态**：Done
- 执行记录 1：删除 `ensureCargoSectionOpen`/widget 子选择器填表；对齐 115541/115059
- 执行记录 2：`mothership.cargo.test.ts` 通过；待 probe 至 scrapeQuotes

### Step 10.21 fillCargo 尺寸区展开 + 减少页面跳动

- **根因**：每字段重复 `scrollIntoView`/重开货运详情；托盘填完后尺寸 placeholder 未展开（录制需点展开钮）
- **修复**：widget 滚动 4s 冷却；`ensureCargoDimensionsVisible`；货物区只开一次；`scrapeQuotes` 内自动点提交
- **状态**：Done
- 执行记录 1：`page-prep.ts` 滚动冷却；`mothership.ts` fillCargo 重构
- 执行记录 2：单测通过；待 `probe:rpa` 验证至 scrapeQuotes

### Step 10.20 派送地址误命中已锁定提货框

- **根因**：`openAddressSide(delivery)` 跳过点「送货至」；`visibleStreetInput` 在仅 1 个 textbox 时误取只读提货框
- **修复**：派送强制双点分区；`deliverySectionOpened` 后才取最后一个可编辑框；`readonly/disabled` 校验
- **状态**：Done
- 执行记录 1：`address-side.ts` 派送路径对齐录制 115059；`mothership.ts` 传 `deliverySectionOpened`
- 执行记录 2：`address-side.test.ts` 通过；待 `probe:rpa` 验证 fillCargo

### Step 10.19 首页 Cookie + 滚动稳定 + headed 不闪退

- **根因**：CookieYes 遮挡 widget；报价区在首屏下方；resolveQuoteEntry 重复 preCheck 导致分区连点跳动；finally 立即关窗
- **修复**：`page-prep.ts` dismissCookie + scrollIntoView；resolveQuoteEntry 仅 stabilize；headed 默认 `shouldKeepBrowserOpen`
- **状态**：Done
- 执行记录 1：新增 `workers/rpa/page-prep.ts`；`address-side`/`quote-page-adapter`/`mothership`/`session-manager` 对齐
- 执行记录 2：`page-prep.test.ts` + `quote-page-adapter.test.ts` 通过；待 `probe:rpa` 实网验证

---

- **根因**：点选后用 `getByText` 复查 label；输入框回显仍含该文案 → 误判下拉未关闭
- **修复**：`isAddressSuggestionDropdownOpen` 检测 `.pac-container`/`listbox` 可见性
- **状态**：Done
- 执行记录 1：移除 `fillAddressField` 点选后 getByText 复查逻辑
- 执行记录 2：未锁定改判下拉容器；失败时补按 Enter 重试

### Step 10.17 RPA 填表地址点选：USA/粘连文案不匹配

- **根因**：用户确认 `display_label` 含 `, USA` 或空格格式；下拉 DOM 为 `StreetLos Angeles, CA`（录制 112133）
- **修复**：`resolveDomSuggestionLabel` 重扫下拉模糊匹配；`labelToCandidate` 统一粘连 display_label
- **状态**：Done
- 执行记录 1：`fillAddressField` 先 wait 再 resolve 后点选
- 执行记录 2：`address-suggestions.test.ts` 粘连/USA 匹配用例通过

### Step 10.16 询价失败展示可读错误原因

- **根因**：RPA 失败仅落库 `QUOTE_UNAVAILABLE`；前端写死通用文案
- **输出**：`quote-error-messages.ts`；`quote_record.error_message`；API `error_message`
- **状态**：Done
- 执行记录 1：`fallback-orchestrator` 保留 RPA 细分 error_code 并写入 error_message
- 执行记录 2：单测 quote-error-messages/serializer/polling 通过

### Step 10.15 MotherShip 联想不含邮编

- **规则**：RPA 仅键入/匹配街道+城市+州+国家；下拉项不展示邮编；zip 保留用户表单值
- **变更**：`formatAddressQuery` 去 zip；`labelToCandidate`/`applyMothershipCandidate` 补全 zip
- **状态**：Done
- 执行记录 1：`mothership.ts` 填表改用 `street, city, state, USA` 触发联想
- 执行记录 2：mock catalog 与单测对齐无邮编 display_label

---

- **根因**：下拉项为 `1234 Warehouse StreetLos Angeles, CA`（无 zip）；采集强制 `includes(90001)` + 仅扫 widget 内 DOM
- **修复**：放宽门牌号/州/城市匹配；全页 `.pac-container` + `page.getByText` 对齐录制
- **状态**：Done
- 执行记录 1：闪退为 `closeBrowser()` 正常行为，非 DOM 变化
- 执行记录 2：`address-suggestions.test.ts` 无 zip 下拉项解析通过

---

- **根因**：`.pac-item`/`[role=option]` 与 MotherShip widget 实际 DOM 不符；录制用 `getByText('1234 Warehouse StreetLos')` 点选
- **修复**：`workers/rpa/address-suggestions.ts` 增 zip/门牌号启发式采集 + widget getByText 回退
- **状态**：Done
- 执行记录 1：`address-candidates.ts`/`mothership.ts` 共用联想等待与点选逻辑
- 执行记录 2：`.env` 增 widget listbox selector；`address-suggestions.test.ts` 粘连格式解析通过

---

### Step 10.30 RPAContext contract validator

- **根因**：`instanceof RPAContext` 失效时 `fromSession(ctx)` 把 Context 当 Page，致 `page.locator` undefined → `.filter` 崩溃
- **修复**：`context-validator` duck-typing + `resolveRPAContext`；steps fail-fast；数组 `ensureArray`
- **状态**：Done
- 执行记录 1：新增 runtime 别名、assertRPAContext、address-side/page-prep 改用 resolveRPAContext
- 执行记录 2：kernel 单测通过；probe 验证 fillAddress 不再 contract 断裂


- **目标**：消除 mothership.ts patch 结构；RPAContext / SelectorGraph / StateEngine / ExecutionLayer
- **输出**：`workers/rpa/kernel/`；mothership 薄编排；cargo-state/page-prep 委托 kernel
- **状态**：Done
- 执行记录 1：新增 context/selector-graph/state-engine/execution-layer/steps/orchestrator；禁止模块级 widgetScrollAt
- 执行记录 2：RPA 相关单测 220+ 通过；fillCargo 状态流迁至 kernel/cargo-step

### Step 10.31 Network-First Quote Architecture

- **目标**：报价仅来自网络层；禁止 DOM price extraction / tab scrape / embedded JSON fallback
- **输出**：`quote-network-capture.ts`、`quote-schema-validator.ts`、`thin-browser-driver.ts`；删除 `dom-fallback.ts`/`client-state.ts`
- **状态**：Done
- 执行记录 1：pipeline 重写为 submit→network capture→strict validate；fail-fast `RPA_DATA_INVALID`
- 执行记录 2：新增 `MOTHERSHIP_QUOTE_API_URL_PATTERNS` pinned endpoint + `RPA_QUOTE_NETWORK_SNIFF` 校准

### Step 10.32 Contract-driven Quote Extraction

- **目标**：报价匹配改为 response shape 契约；URL/pinned 仅 telemetry；Quote Contract Guard fail-fast
- **输出**：`quote-contract-guard.ts`；`contractPayloads`；移除 URL runtime matching
- **状态**：Done
- 执行记录 1：契约必填 `rates.standard.lowest`、`rates.fastest|standard.fastest`、`rates.guaranteed`
- 执行记录 2：禁止 partial decode fallback；`source=contract`；pinned 仅 `formatPinnedTelemetry`

### Step 10.33 首页精简 widget cargo 展开

- **根因**：`mothership.com/` 首页 widget 为「What are you shipping?」汇总，无独立 pallet 输入；`assertCargoReady` 仅认 pallet → `no_cargo_inputs`
- **修复**：展开货物面板 + `weight|pallet` 就绪判定 + Cookie Accept 按钮
- **状态**：Done
- 执行记录 1：`state-engine.triggerCargoSection` 增首页标签点击；`WAIT_CARGO_INTERACTIVE` 改用 cargoReadyRe
- 执行记录 2：`isCargoStateReady` 单测；`selector-specs` 增 What are you shipping 回退

### Step 10.34 cargo summary chip → drawer UI flow

- **根因**：cargo UI 为 summary chip，须 click 才展开 drawer；input 探针在 chip 未展开时恒为空
- **输出**：`workers/rpa/cargo-editor.ts`；`fillCargoStep` 改为 expand → wait drawer → fill in drawer
- **状态**：Done
- 执行记录 1：新增 `expandCargoEditor`/`waitForCargoEditorOpen`；就绪条件改为 dialog/modal 可见
- 执行记录 2：不再调用 `triggerCargoSection`/`assertCargoReady`；state-engine/contract/network 未改

### Step 10.35 cargo popover 检测修复

- **根因**：`cargo-editor.ts:19-26` `isCargoEditorOpen` 仅认 `role=dialog`；MotherShip 为无 dialog 的 popover（Weight/L/W/H）
- **修复**：popover 以 ADD ITEM/Weight placeholder 为打开信号；chip 点击增 `getByRole(button,/Pallet/)` + force click
- **状态**：Done
- 执行记录 1：失败截图 `rpa-failure-2026-06-17T09-37-51-321Z.png` 证实 chip 已填地址后显示 1 Pallet
- 执行记录 2：仅改 `workers/rpa/cargo-editor.ts`；日志 `[rpa] cargo-editor: popover opened`

### Step 10.36 中文精简 widget 跳过 cargo 填表

- **根因**：`cargo-editor.ts:185` 对无 Pallet chip 的中文 widget 仍强制 expand；DOM 诊断证实填址后 widget 无 cargo input（仅「货运详情」步骤文案）
- **修复**：`isCargoUiPresent()` 为 false 时 skip expand/fill；英文 chip 流保留原逻辑
- **状态**：Done
- 执行记录 1：`scripts/debug-cargo-chip.ts` 证实 headless 中文 UI 无 pallet/weight 节点
- 执行记录 2：probe 已通过 fillCargo；下一阻塞为 submit 后 `contract=0`（network 层，另案）

### Step 10.37 quote shape normalize

- **根因**：`quote-contract-guard.ts` `findRatesObject` 仅浅层 `body.rates`；API 返回 `data.rates` 等变体 → `satisfiesQuoteContract` 恒 false → `contract=0`
- **输出**：`quote-normalize.ts`（resolveRatesFromBody/normalizeQuote）；契约仅校验 CanonicalQuote
- **状态**：Done
- 执行记录 1：`quote-step.ts` 导出 resolver；capture 用 `normalizeQuote` 过滤；`RPA_QUOTE_DEBUG_RAW` 失败日志
- 执行记录 2：单测覆盖 data.rates/result.rates/[0].rates

### Step 10.38 quote network visibility 诊断层

- **根因**：`json=0` 表示 submit 后无 JSON 候选进入 capture；需证明 A/B/C（XHR / navigation / 无请求）
- **输出**：`quote-network-visibility.ts`；capture drain inflight；`scripts/debug-quote-network.ts`；失败时 `quote-debug` 打 trace summary
- **状态**：Done
- 执行记录 1：`RPA_QUOTE_NETWORK_VISIBILITY=true` 全量 REQ/RESP；post-submit url 日志；修复 async handler 竞态
- 执行记录 2：`.env.example` 增 visibility 开关；不改 kernel/contract/selector

### Step 10.39 Business Ready Gate（submit 前闸门）

- **根因**：submit 点击无 quote 请求；Cookie 遮挡 + 表单未真正 ready 导致 inert click
- **输出**：`quote-business-ready.ts`（waitForBusinessReadyState/debugBusinessStateSnapshot）；`thin-browser-driver` 闸门拦截
- **状态**：Done
- 执行记录 1：四门检测（button/cookie/cargo lock/hydration+DOM stable）+ pointer hit-test
- 执行记录 2：单测 `quote-business-ready.test.ts`；未改 kernel/contract/network capture

### Step 10.40 cargo 锁定（英文入口 + 强制填表）

- **根因**：中文精简 widget 无 cargo editor → skip 填表 → business-ready 货物未锁定
- **修复**：`RPA_BROWSER_LOCALE=en-US` + `get-a-quote` URL；`fillCargoStep` 禁止 skip；`cargo-lock.ts` 关闭 drawer 并校验 pallet 摘要
- **状态**：Done
- 执行记录 1：`session-manager` context locale/Accept-Language；`expandCargoEditor` 无 UI 时 fail-fast
- 执行记录 2：`.env` 更新 URL/locale/submit selector；`cargo-lock.test.ts` 通过

### Step 10.41 中文内联货物步填表与锁定

- **根因**：`货运详情` 展开为 widget 内联 input 非 popover；`locator(placeholder=)` 无效；摘要为 `2 托盘s`
- **修复**：`isCargoFieldsVisible`/`resolveSpecOnScope(getByPlaceholder)`；`settleAddressFieldsForCargo`；摘要 regex 含托盘s
- **状态**：Done
- 执行记录 1：`expandCargoEditor` 点击货运详情步骤标签；`fillInCargoEditor` 修正 placeholder 定位
- 执行记录 2：`isCargoSummaryLocked` 认摘要文案+inputValue；business-ready 同步托盘s 模式

### Step 10.42 Cargo Commit Barrier

- **根因**：summary chip 可见但 placeholder 仍可编辑 → 假锁定 → submit 无 quote flow
- **输出**：`waitForCargoCommittedState`/`assertCargoCommittedState`；错误码 `CARGO_NOT_COMMITTED`
- **状态**：Done
- 执行记录 1：committed 需 summary + 无 editable placeholder + internal pallet/weight 值
- 执行记录 2：`fillCargoStep`/`submitQuoteForm`/`business-ready` 统一走 commit barrier

### Step 10.43 重量提交与 committed 判定修复

- **根因**：重量未 commit → 摘要 `-Lbs`；`226Lbs` 无空格未匹配；wait 轮询 `attemptCargoCommit` 重置向导
- **修复**：重量单独填+Tab commit；摘要完整即 committed；移除 wait 轮询 commit；widget-only 填表
- **状态**：Done
- 执行记录 1：`evaluateCargoCommitted` 识别 `226Lbs`；拒绝 `-Lbs`；内联步摘要完整可放行
- 执行记录 2：`debug-post-submit` submit 跳转 `sign-up-quote?quote-details=...`

### Step 10.44 货物 UI 等待与 business-ready 地址闸门

- **根因**：地址填完后货物步渲染延迟 → `isCargoUiPresent` 立即 fail；`probeWidgetDom` 地址判定与内联货物步不一致
- **修复**：`waitForCargoUiPresent` + `triggerCargoSection`；`isCargoWizardPastAddress` 统一地址锁定判定
- **状态**：Done
- 执行记录 1：`fillCargoStep` 轮询等待货物 UI，去除立即 `QUOTE_ENTRY_UNAVAILABLE`
- 执行记录 2：`quote-business-ready` 用 `isCargoWizardPastAddress` 替代 DOM 启发式

### Step 10.46 双地址强闸门与 submit 跳转校验

- **根因**：`isAddressesLockedForSubmit` 单州码弱放行 → submit 无跳转 → `contract=0 json=0`
- **修复**：双州码/双邮编/双街道强判定；`fillAddressStep`/`submitQuoteForm` 双地址 assert；submit 未跳转 fail-fast；等待 `axel/quote`
- **状态**：Done
- 执行记录 1：`evaluateAddressesLockedForSubmit` 移除 `stateCount>=1`；business-ready 去掉 cargo 绕过地址
- 执行记录 2：`submitQuoteForm` 未跳转抛错；`quote-network-capture` 等待 axel/quote 响应

### Step 10.47 Worker Redis 稳定性与全栈验收

- **根因**：WSL 休眠致 Redis 间歇断开；`maxRetriesPerRequest:3` 致心跳抛错；此前仅 probe 不构成 worker 验收
- **修复**：`family:4` + `maxRetriesPerRequest:null`；worker 自动拉起 WSL infra + Redis 恢复；`verify:rpa-stack` 四步验收；WSL keepalive
- **状态**：Done
- 执行记录 1：加强 `verify:rpa-stack` worker 步：30s 观察 + 首次心跳后再稳 15s + 检测 MaxRetries/心跳失败
- 执行记录 2：`npm run verify:rpa-stack` 四步全绿；`start-wsl-infra` 增加 WSL keepalive 防休眠

### Step 10.48 Redis 主机解析与验收环境同步

- **根因**：WSL localhost 转发间歇失效；`verify:rpa-stack` 未加载 `.dev/infra.env`；`Resolve-InfraHost` 仅 TCP 探测优先 127.0.0.1
- **修复**：优先 WSL IP + Node 新建连接 PING；`loadDevEnv` 双文件加载；spawn 合并 infra.env；worker 前摇探针 + BullMQ error handler
- **状态**：Done
- 执行记录 1：`lib/dev-env.ts` + `Resolve-InfraHost` 改用 `Test-RedisPingFromNode`；`infra.env` 写入 `WSL_HOST_IP`
- 执行记录 2：`verify:rpa-stack` spawn 前 `probeFreshRedisConnection`；`ensureRedisReady` 30 次重试 + WSL 唤醒

### Step 10.49 反爬伪装与 sign-up-quote 落点修复

- **根因**：SPA 跳转不触发 `waitForURL` 误报未跳转；Playwright Chromium 自动化指纹；缺少首页预热
- **修复**：`post-submit` URL 轮询；`playwright-extra` stealth + `RPA_BROWSER_CHANNEL`；`RPA_HUMAN_ENTRY_FLOW`；`RPA_NETWORK_DEBUG`
- **状态**：Done
- 执行记录 1：`lib/rpa/browser-launch.ts` stealth/patchright/chrome；`thin-browser-driver` 接受 sign-up-quote
- 执行记录 2：`quote-page-adapter` 首页预热；sign-up-quote 页延长 network settle；单测 post-submit-navigation

### Step 11.14 MotherShip listbox 多候选采集修复

- **根因**：`enumerateAddressCandidates` 先用含 USA 的完整 query 仅得 1 项；`labelToCandidate` L355-371 解析失败回退用户输入生成 DrDallas/BlvdLos；listbox 双行未转 `主|副`；`parsePacStructuredLabel` 误把 USA 当州码
- **修复**：`buildAddressEnumerationQueries` 粘连 query 优先；`COLLECT_WIDGET_SUGGESTION_LABELS`；删除 query 回退；`option_id` 去重
- **状态**：Done
- 执行记录 1：`browser-scripts.ts`/`address-suggestions.ts` widget 采集与枚举链路
- 执行记录 2：`address-candidates.ts` option_id 去重；单测 Garland/TX/粘连 query

### Step 11.13 Redis WSL IP 失效回退 127.0.0.1

- **根因**：`.dev/infra.env` 写入过期 WSL IP `172.26.233.197`，覆盖 `.env` 的 `127.0.0.1`；WSL 休眠后 Connection is closed
- **修复**：`Resolve-InfraHost` 优先 127.0.0.1；`buildRedisCandidateUrls` + `ensureRedisReady`/`withBoundedRedis` 自动回退
- **状态**：Done
- 执行记录 1：`lib/redis.ts` 多 URL 探针与 adoptRedisUrl
- 执行记录 2：`ensure-infra.ps1` 解析顺序调整；`.dev/infra.env` 改回 127.0.0.1

### Step 11.12 pac-item 多候选 suburb 解析修复

- **根因**：innerText 粘连 + `labelToCandidate` 将 Garland/Wilmer 均解析为 query.city(Dallas)，`display_label` 去重后只剩 1 项
- **修复**：`COLLECT_PAC_ITEM_LABELS` 主行|副行采集；`parsePacStructuredLabel` 以 suburb 为 city；去重改 raw label
- **状态**：Done
- 执行记录 1：`browser-scripts.ts`/`address-suggestions.ts` 结构化 pac 采集与解析
- 执行记录 2：`address-candidates.ts` 去重键；单测 Garland/Wilmer/Farmers Branch

### Step 11.11 地址候选 180s 边界超时修复

- **根因**：`enumerateAddressCandidates` 全量 query 各 12s × 双侧超 180s，前端 Abort；Redis 断连时轮询读不到 worker 结果
- **修复**：主 query 12s + 最多 2 个短 query 5s，≥2 项即停；超时链提至 210s；Redis 探针与不可用快失败
- **状态**：Done
- 执行记录 1：`address-suggestions.ts` 智能枚举；`constants`/`route` 210s/300s
- 执行记录 2：`candidates-queue.ts` ensureRedisReady + redis_unavailable 区分

### Step 11.10 多候选枚举与报价超时对齐

- **根因**：联想首条即返回漏采其余 pac-item；前端/后端报价超时 60s，RPA 候选+询价合计超 60s
- **修复**：`enumerateAddressCandidates` 多 query 合并 + 稳定等待；`QUOTE_TIMEOUT_MS`/`POLL_TIMEOUT_MS`/`RPA_JOB_TIMEOUT_MS` 提至 150s/150s/120s
- **状态**：Done
- 执行记录 1：`address-suggestions.ts` 稳定采集与 pac-item 选择器优化
- 执行记录 2：`address-candidates.ts` 改 `enumerateAddressCandidates`；超时链与单测同步

### Step 11.9 地址确认弹窗强制展示与展示去邮编

- **根因**：`requires_selection=false` 时前端跳过确认弹窗自动询价；`CandidateGroup` 单候选不渲染；本地 mock `formatted_address` 含邮编
- **修复**：RPA 返回后一律弹出确认窗；单候选也展示待确认项；`formatAddressDisplayLabel` 展示不含邮编
- **状态**：Done
- 执行记录 1：`embedded-quote-widget.tsx` 移除自动择一；`address-disambiguation-modal.tsx` 单候选可见
- 执行记录 2：`format-address.ts` + `quote-form.tsx` 已选择文案去邮编；mock catalog 同步

### Step 11.8 地址候选与询价统一 RPA Worker 链路

- **根因**：子进程 spawn 与 worker 双浏览器争用 `.rpa/mothership-storage.json`；spawn 路径可联想不可报价，worker 路径可报价不可联想
- **修复**：候选 job 入 BullMQ 同一 worker；`withRpaSessionLock` 串行；`address-candidates` 复用 `openQuotePage`/`address-side`/`address-suggestions`；API 轮询 Redis 结果
- **状态**：Done
- 执行记录 1：`candidates-queue.ts`/`candidates-job-handler.ts`/`queue.ts`/`index.ts` 接线；`mothership-candidates.ts` 改 worker 队列
- 执行记录 2：`address-candidates.ts` 对齐 quote 会话链路；移除 `maxQueries:2`；单测 mock worker 队列

### Step 11.4 dev:start prisma generate EPERM 修复

- **根因**：旧 next/worker 进程占用 `query_engine-windows.dll.node`；Stop 后仅等 500ms，Windows 未及时释放 DLL
- **修复**：`Wait-DevProjectProcessesGone` + `Wait-PrismaEngineWritable` + `Invoke-PrismaGenerateSafe` 重试
- **状态**：Done
- 执行记录 1：`lib.ps1` 增强进程识别/kill 与 engine 可写探测；`start.ps1` 改用安全 generate
- 执行记录 2：失败时清理 `.tmp` 引擎文件并提示 `npm run dev:stop`

### Step 11.7 地址联想前端 Abort 超时与派送框定位

- **根因**：RPA ~119s 与前端 Abort 120s 竞态；`getVisibleStreetInput` 未传 `deliverySectionOpened` 派送侧反复失败
- **修复**：超时链 180s/150s；派送框定位；候选 `maxQueries:2`；限流 bounded Redis
- **状态**：Done
- 执行记录 1：`address-side.ts`/`constants`/`route.ts` 超时与 RPA 优化
- 执行记录 2：`rate-limiter.ts`/`ip-rate-limiter.ts` 使用 `withBoundedRedis`

### Step 11.6 地址候选 API Playwright 子进程隔离

- **根因**：Next API 直接 import Playwright → `ERR_INVALID_FILE_URL_PATH` 500 → 前端「响应解析失败」
- **修复**：`spawnFetchMothershipAddressCandidates` 子进程跑 `scripts/rpa/fetch-address-candidates.ts`
- **状态**：Done
- 执行记录 1：`mothership-candidates.ts` 改 spawn；新增 `lib/rpa/spawn-address-candidates.ts`
- 执行记录 2：`api-client.ts` 非 JSON 友好提示；route 捕获非 RpaError 返回 503 JSON

### Step 11.5 dev:start Prisma stderr 误杀脚本

- **根因**：`$ErrorActionPreference=Stop` 下 node stderr 警告被 PowerShell 当 NativeCommandError 终止；非 EPERM
- **修复**：`Invoke-DevNativeCommand`/`Invoke-NpmScript`；新增 `prisma.config.ts` 消除 package.json#prisma 警告
- **状态**：Done
- 执行记录 1：`lib.ps1`/`start.ps1` 外部命令统一 Continue 捕获 exit code
- 执行记录 2：`package.json` 移除 prisma 块；seed 迁入 `prisma.config.ts`

### Step 11.3 MotherShip 地址联想超时修复

- **根因**：Headless 下 quoter widget 骨架屏未 hydration 即填表；Google Places 响应未等待；5s 联想超时过短；Redis 单例无限重试拖慢候选 API
- **修复**：`waitForQuoterWidgetHydrated` + `preCheck`；Places 网络/pac-item 等待；候选联想 12s；bounded Redis 写入
- **状态**：Done
- 执行记录 1：`page-prep.ts`/`address-side.ts`/`address-suggestions.ts`/`address-candidates.ts`/`quote-session-store.ts`
- 执行记录 2：`page-prep.test.ts` hydration 探针用例；待实网验证候选 API 多选弹窗

### Step 11.18 返回 MotherShip 全部报价档位

- **根因**：`payload-decode`/`quote-schema-validator` 强制归一化为 4 档，`bestValue` 被丢弃或合并
- **修复**：扩展 `rateOption=bestValue`；归一化保留核心 4 档 + 可选 bestValue；前端动态展示
- **状态**：Done
- 执行记录 1：`quote-completeness`/`quote-schema-validator`/`payload-decode` 支持 4~6 档
- 执行记录 2：`quote-card` 动态 rate tab；单测 axel fixture 含 standard.bestValue

### Step 11.20 MotherShip 官网 Widget 档位对齐 + 专属卡车

- **根因**：仅解析 standard/guaranteed；Standard 默认展示 API absolute lowest 与官网「最低价格」(bestValue) 不一致；`rates.dedicated` 未接入
- **修复**：`lib/constants/mothership-ui-tiers.ts` 可扩展 Tab 注册表；`payload-decode` 动态解析 rates 顶层键；`prepareMotherShipStorageQuotes` 统一入库/API/前端过滤
- **状态**：Done
- 执行记录 1：`dedicated` 档解析 + `quote-card` 三 Tab；Standard 并存 lowest/bestValue 时隐藏 absolute lowest
- 执行记录 2：`mothership-ui-tiers.test.ts` + `quote-capture` dedicated 用例；`tsc --noEmit` 通过

### Step 11.19 「已选择」地址展示逻辑修复

- **根因**：`quote-form` 在本地 mock 联想点选后即显示「已选择」，未等 MotherShip 弹窗确认
- **修复**：仅 `selected_from_mothership=true` 时展示；弹窗确认后由 widget 回传 confirmed 地址
- **状态**：Done
- 执行记录 1：`AddressSection` 改读 `confirmed` prop，不再用本地 `selected` 展示
- 执行记录 2：`embedded-quote-widget` 确认后 setConfirmedPickup/Delivery，改地址草稿时清除


- **根因**：UI 走 `requestMothershipCandidatesViaWorker` BullMQ 轮询；WSL Redis 间歇断开时 job 入队但 worker 无法消费/写回结果，API 空轮询 210s；`probe:rpa` 直连 RPA 不经队列故正常
- **修复**：`isAnyRpaWorkerHealthy` + 队列 waiting 25s stall 检测；不可用时回退 `spawnFetchMothershipAddressCandidates`
- **状态**：Done
- 执行记录 1：CLI 实网 pickup 4 / delivery 2 约 112s；verify-address-candidates 142s 成功（spawn 回退）
- 执行记录 2：`candidates-queue.test.ts` + `worker-health.test.ts` 4/4 通过

---

### Step 11.16 街道-only 联想 label 解析失败

- **根因**：`listSideCandidates` L91-96；MotherShip 返回 `1234 Warehouse Street` 等仅街道行，`labelToCandidate` L377 返回 null
- **修复**：`parseStreetOnlySuggestion` 补全 city/state；pipe 行 `isMotherShipEnumerationRow` 不再误杀 streetToken
- **状态**：Done
- 执行记录 1：`address-suggestions.ts` 新增 street-only 解析；pipe 副行 echo 判定
- 执行记录 2：CLI 实网 pickup 3 项 / delivery 2 项；单测 16/16

### Step 11.15 地址枚举 ArrowDown 与 echo 过滤修复

- **根因**：枚举路径 `ArrowDown` 收起 listbox 仅留 1 项；`isMotherShipEchoCandidate` 仅比 display_label 把不同街道/城市候选误杀
- **修复**：`forEnumeration` 跳过 ArrowDown；listbox 可见等待；echo 需 street+city 均匹配；pipe 标签二次吸收
- **状态**：Done
- 执行记录 1：`address-suggestions.ts` 枚举专用 trigger + echo 判定放宽
- 执行记录 2：`address-candidates.ts` 空候选时 pipe 标签跳过 echo；单测 14/14

### Step 11.0 阶段0 Network-First 录证脚本

- **输入**：方案 A 批准；GD-15 匿名入口 URL
- **输出**：`scripts/phase0/record-baseline.ts` + `verify-baseline.ts` + `docs/phase0-recording-checklist.md`
- **产物**：`.rpa/phase0/baseline.har`、`quote-contract.schema.json`、`session-storage-snapshot.json`（人工执行后生成）
- **约束**：独立脚本，不修改 workers/rpa 任何代码
- **状态**：Done（脚本就绪；verify:phase0 7/7 PASS）
- 执行记录 1：headed 双检查点录证（地址锁定→存储快照→提交报价→HAR+Schema）
- 执行记录 4：A3 误报修复；axel/quote 契约为 data.rates + availableRates

### Step 11.2 阶段1 单会话 QuoteSession

- **输入**：Step 11.1 axel 契约；阶段0 session-storage-snapshot
- **变更**：`quote-session-store` Redis+文件；候选 API 返回 `quote_session_id`；询价复用 storageState
- **验证**：quote-session-store 单测；候选→询价 session 透传
- **状态**：Done
- 执行记录 1：`address-candidates` 保存 session + persistContext；`openQuotePage(sessionId)` 加载
- 执行记录 2：前端/API `quote_session_id` 透传；`quote-mapper`/`mothership` 复用会话

### Step 11.1 阶段1 axel/quote Contract-Driven Capture

- **输入**：quote-contract-summary.json；阶段0 录证 7/7
- **变更**：`axel-contract.ts`；guard 接受 guaranteed.bestValue；decode 映射 bestValue/days；axel URL 优先
- **验证**：`quote-capture.test.ts` axel fixture 四档解析通过
- **状态**：Done
- 执行记录 1：`quote-contract-guard` lowest/bestValue + baseQuotePrice/finalPrice 价格键
- 执行记录 2：`payload-decode` guaranteed.bestValue→lowest；axel URL 排序优先；单测通过

### Step 10.50 sign-up-quote 匿名流与 Chrome channel

- **根因**：匿名成功落点为 `sign-up-quote`（非登录墙）；Playwright HeadlessChrome 指纹致 `axel/quote` 不返回；`waitForURL` SPA 误报
- **修复**：URL 轮询接受 sign-up-quote；`RPA_BROWSER_CHANNEL=chrome` + sign-up 页 18s widget 等待；捕获 `services.mothership.com/axel/quote`
- **状态**：Done
- 执行记录 1：`RPA_USE_STEALTH=false`（stealth 破坏 widget）；`probe:rpa` 在 chrome channel 下 4 档全绿
- 执行记录 2：网络日志确认 HeadlessChrome 上报；sign-up-quote 后 widget 触发 axel/quote

### Step 11.19 报价解析精度与地址枚举 query 修复

- **根因 A**：`payload-decode` `walkJson` 递归误采嵌套对象；`readPrice` 未优先 `finalPrice`；`readTransit` 将 `days:0` 格式化为 0 business days；`normalizeQuoteItems`/`normalizeToFourTiers` 复制 lowest 伪造 fastest
- **根因 B**：`buildAddressEnumerationQueries` 粘连 query 首条且无 USA；枚举次 query 仅 5s 超时
- **修复**：结构化解析 `rates.{standard,guaranteed}.{lowest,fastest,bestValue}`；只返回 API 真实档位；枚举首条 `formatAddressQuery`；统一 12s 超时
- **状态**：Done
- 执行记录 1：`payload-decode.ts`/`quote-schema-validator.ts`/`quote-completeness.ts` 最小改动
- 执行记录 2：`address-suggestions.ts` 枚举 query 顺序与超时对齐 autocomplete；单测更新通过

### Step 11.20 邮编可选 + 准确度测试从组 01 重跑

- **变更**：询价 API `zip` 改为可选（空字符串通过）；测试脚本 10 组地址不再填邮编
- **状态**：Done
- 执行记录 1：`modules/quote/validation.ts` zip 校验与 address lookup 对齐
- 执行记录 2：`generate-accuracy-test-data.ts` 去掉 zip 字段，清空进度从组 01 重跑

### Step 11.21 报价 UI 档位对齐 + 地址枚举 filter 修复

- **根因 A**：`payload-decode.ts` 提取 `guaranteed` 档且 `readPrice` 未锁定 `finalPrice`；`normalizeQuoteItems` 输出含 guaranteed 四档
- **根因 B**：`collectEnumerationLabels`（592 行）`isMotherShipEnumerationRow` 过严，真实 pac-item 被滤空；`ENUMERATE_MAX_QUERIES=3` 截断 query
- **修复**：仅输出 `UI_QUOTE_TIERS`（standard×3）；`finalPrice` 优先；枚举 query 与 autocomplete 统一；filter 空时回退 merged
- **状态**：Done
- 执行记录 1：`payload-decode.ts`/`quote-schema-validator.ts`/`quote-completeness.ts`/`lib/constants/quote.ts`
- 执行记录 2：`address-suggestions.ts` 枚举 filter 回退 + query 统一；单测 39/39 通过

### Step 11.22 准确度测试对齐用户 HTTP 查价路径

- **变更**：`generate-accuracy-test-data.ts` 弃用 Worker 直连/Redis 轮询，改用 `hostFetchMothershipCandidates` + `hostCreateQuote` + `pollQuoteUntilDone`（与 EmbeddedQuoteWidget 一致）
- **状态**：Done
- 执行记录 1：候选/询价/轮询均走 Next HTTP API + x-customer-id 头
- 执行记录 2：确认候选用 `applyMothershipCandidate`；启动前检查 Next 就绪

### Step 11.23 地址 WhenStable 采集 + finalPrice 防污染 + 超时 300s

- **根因 A**：`enumerateAddressCandidates` 801 行 `waitForAddressSuggestions` 误报 false 时 `continue` 跳过采集（Headless 下 DOM 已有联想）
- **根因 B**：`resolveTierObject` 356 行用 `availableRates` extended/customRate 覆盖 `data.rates.standard.*.finalPrice`
- **根因 C**：`CANDIDATES_FETCH_TIMEOUT_MS`/`ADDRESS_CANDIDATES_TIMEOUT_MS`=210s < 实际 RPA ~285s
- **修复**：改用 `collectAddressSuggestionLabelsWhenStable`；叶节点有 finalPrice 则不合并 extended；超时 300s
- **状态**：Done
- 执行记录 1：`address-suggestions.ts` 枚举去掉 waitFor 门控；`payload-decode.ts` resolveTierObject 防污染
- 执行记录 2：超时常量 300s；单测覆盖 $287/$345/$457 finalPrice 场景

### Step 11.20 RPA 询价链路性能优化（P0~P4）

- **目标**：缩短候选→询价端到端耗时，不影响 RPA 功能与四档验收
- **变更**：Browser 保活；quote_session 复用跳过预热/重复填址；收紧 sign-up settle；自适应轮询
- **验证**：address-step/env/quote-page-adapter/use-quote-polling 单测通过
- **状态**：Done
- 执行记录 1：`shouldKeepBrowserWarm` 默认 headless 保活；`address-candidates` 不再每次 `closeBrowser`；`fillAddressStep` session fast-path
- 执行记录 2：`RPA_SESSION_WIDGET_TIMEOUT_MS=15s`；`RPA_SIGN_UP_QUOTE_SETTLE_MS=10s`；轮询 10s 后改 800ms

### Step 11.21 候选+询价单次 RPA 会话（驻留页 + 并行预热）

- **目标**：候选完成后驻留 page/context；用户确认弹窗期间预热；询价同页续跑不重开
- **变更**：`parked-quote-session` 池；候选 park；询价 take；preheat/release API + 前端弹窗联动
- **验证**：parked-quote-session 单测；驻留不可用回退 storageState 开页
- **状态**：Done
- 执行记录 1：`address-candidates` park 后不关页；`mothership` 优先 `takeParkedQuoteSession`
- 执行记录 2：preheat/release BullMQ job；embed 弹窗打开预热、取消释放

### Step 11.24 队列卡死 + 询价超时 + preheat jobId

- **根因 A**：Redis 队列存在 orphan `active` address-candidates job，阻塞 Worker；`QUEUE_STALL_MS=25s` 在 Worker 忙时 spawn 第二套 Playwright
- **根因 B**：`QUOTE_TIMEOUT_MS`/`RPA_JOB_TIMEOUT_MS`=150s/120s < 实测 RPA ~285s → 测试/前端先报 timeout、DB 孤儿 processing
- **根因 C**：`addParkedSessionJob` jobId 含 `:` → BullMQ 抛 `Custom Id cannot contain :`，preheat 500
- **修复**：stall 改 60s 且 active>0 不 spawn；超时 300s；jobId 改 `_`；Worker stalledInterval
- **状态**：Done
- 执行记录 1：`candidates-queue.ts`/`queue.ts`/`quote.ts`/`rpa.ts`/`workers/rpa/index.ts`
- 执行记录 2：清 orphan active job；单测更新；待重启 worker+next 后仅跑组 01 验证

### Step 11.26 动态 UI 档位：MotherShip 返回多少输出多少

- **需求**：不同地址可能是 2+1 / 2+2 / 3+3 等，禁止固定 3+2 白名单过滤
- **修复**：`extractTiersFromServiceBlock` 动态遍历 rates 子键；`normalizeQuoteItems`/`normalizeToFourTiers` 全量去重排序
- **状态**：Done
- 执行记录 1：`payload-decode.ts`/`quote-schema-validator.ts`/`quote-completeness.ts`/`quote.ts`
- 执行记录 2：单测覆盖 2+1/2+2/3+3/5 档；仍只读 rates 叶节点 finalPrice

### Step 11.27 E2E 运行环境：去 spawn 双开 + orphan 回收 + 360s 超时

- **根因**：Worker 忙时 stall spawn 双开 Playwright；orphan active job 阻塞队列；300s 轮询不足
- **修复**：仅 Worker 不可用时 spawn；`recoverOrphanActiveJobs` 启动回收；并发 1；超时 360s；测试脚本全栈就绪检查
- **状态**：Done
- 执行记录 1：`candidates-queue.ts`/`queue-recovery.ts`/`index.ts`/`generate-accuracy-test-data.ts`
- 执行记录 2：`QUOTE_TIMEOUT_MS`/`RPA_JOB_TIMEOUT_MS`=360s；`pollQuoteUntilDone` 支持自定义 timeout

### Step 11.28 sweeper 宽限期：420s 超时避免误杀 active RPA job

- **根因**：`QUOTE_TIMEOUT_MS` 与 `RPA_JOB_TIMEOUT_MS` 同为 360s，sweeper 在 RPA 完成前 `moveToFailed` active job
- **修复**：`QUOTE_TIMEOUT_MS`=420s（RPA 360s + 60s grace）；lock +15s；测试脚本启动时 orphan 回收
- **状态**：Done
- 执行记录 1：`lib/constants/quote.ts`/`queue.ts`/`generate-accuracy-test-data.ts`
- 执行记录 2：单测 timeout 阈值 421s；轮询默认跟随 `QUOTE_TIMEOUT_MS`

### Step 11.29 WSL keepalive：长跑 RPA 期间 Redis/MySQL 不断连

- **根因**：`dev:start` 未启 `wsl-keepalive`，WSL sleep 导致 6379/3307 ECONNREFUSED，队列 job 无法消费
- **修复**：`Ensure-DevInfra`/`Start-WslInfra` 启动 keepalive；测试脚本每案前 `ensureInfraStableForCase`
- **状态**：Done
- 执行记录 1：`scripts/dev/ensure-infra.ps1` 两处 `Start-WslKeepAlive`
- 执行记录 2：`generate-accuracy-test-data.ts` 双次就绪探测 + 案前 orphan 回收

### Step 11.25 验收对齐截图：UI 五档 + finalPrice + 基线地址

- **验收**：标准 Tab 3 档（$287.23/$345/$369.72）+ 保证送达 2 档（$457.36/$480.37）
- **修复**：`UI_QUOTE_TIERS` 扩至 5；`extractTiersFromRates` 解析 guaranteed；`resolveUiTierLeaf` 禁止 availableRates 合并
- **状态**：Done
- 执行记录 1：`payload-decode.ts`/`quote.ts`/`quote-schema-validator.ts`/`quote-completeness.ts`
- 执行记录 2：组 01 地址改为 Warehouse Street + Distribution Way Farmers Branch TX；单测五档验收


- **原则**：不缩短第三方 API 等待；填表保留 input/change + fill 校验回退；填表后 100~300ms 随机延迟
- **变更**：`RPA_SIGN_UP_QUOTE_SETTLE_MS` 恢复 18s；新增 `lib/rpa/human-pacing.ts`；地址/货物填表后 `awaitHumanPacing`
- **验证**：integration 42/42；RPA 相关单测 30/30
- **状态**：Done
- 执行记录 1：耗时瓶颈在 page.goto/widget hydration 与 axel/quote 网络响应，非本地 fill
- 执行记录 2：address-suggestions 已有 dispatchEvent+sequential 回退；cargo-editor fill 后加人类节奏

### Step 11.30 单次 RPA 会话 confirm 闭环 + RPA_WORKER_ID 修复

- **根因 A**：`.env` 中 `RPA_WORKER_ID` 为空 → `canPersistParkedQuoteSession()` 恒 false → 候选从不 park
- **根因 B**：前端确认后直接 `submitQuote`，未在同页锁定双地址 → 询价仍完整 `fillAddress` + `openQuotePage`
- **根因 C**：spawn 子进程候选时 park 在子进程内存，退出即失效
- **修复**：worker 启动写入 `process.env.RPA_WORKER_ID`；`hostConfirmMothershipAddresses` + embed confirm API；`RPA_PARKED_SESSION≠false` 禁止 spawn
- **状态**：Done
- 执行记录 1：`workers/rpa/index.ts` 自动设置 RPA_WORKER_ID；`embedded-quote-widget` 确认后调 confirm API
- 执行记录 2：`api-client` 恢复 preheat + 新增 confirm；测试脚本与 candidates-queue 单测对齐

### Step 11.31 放弃驻留页默认路径，改稳定双阶段 + storageState

- **决策**：驻留页需 worker+confirm+同进程，验证周期 >5min 且 fetch failed 频发；MotherShip 网络等待占 80%+，驻留页收益有限
- **变更**：`RPA_PARKED_SESSION` 默认关（仅 `=true` 时启用）；去掉前端 confirm 往返；候选只产 storageState，询价 job 一次开页填址+提交
- **状态**：Done
- 执行记录 1：`isParkedSessionEnabled()` 统一开关；embed/测试脚本不再调 confirm API
- 执行记录 2：保留 browser warm + reuseSession preCheck 快路径；spawn 回退恢复可用

### Step 11.33 货物单托重量语义 + session 残留覆写

- **根因**：`quote_session` storageState 残留 widget 货物摘要（如 weightLbs=2296）；commit 闸门只验托盘数不验单托重量
- **修复**：`cargo-weight.ts` 单托语义校验；`fillCargoStep` 检测残留 → 关闭 editor → forceClear 覆写 → commit 后断言重量
- **状态**：Done
- 执行记录 1：新增 `probeWeightMatchesExpected` / `isStaleCargoState`；`waitForCargoCommittedState` 支持 `ExpectedCargoCommit.weight`
- 执行记录 2：`fillInCargoEditor` 增加 `forceClear`；`cargo-step` 失败重填单托重量；单测 `cargo-weight.test.ts`

### Step 11.38 fillCargoInDrawer 动态字段 + 浏览器防翻译

- **变更**：`fill-cargo-in-drawer.ts` 废弃静态 WEIGHT/LENGTH 选择器；browser TranslateUI 禁用 + locale en-US + notranslate
- **状态**：Done
- 执行记录 1：`fillCargoInDrawer` 动态 placeholder 填托数/重量/尺寸 + Tab + 摘要断言
- 执行记录 2：`cargo-step` 仅调用 fillCargoInDrawer；`RPA_CARGO_DRAWER_PAUSE` 控制 page.pause

### Step 11.37 重量单位延迟至填表前解析 + 字段等待

- **根因**：`buildExpectedCargo` 在重量字段未渲染时默认 lb，内联 kg 字段填 lb 值失败
- **修复**：`fillNewFreightLine` 填完尺寸后再 `resolveMotherShipWeightDisplayFromPage`；`waitForFieldLocator` 12s
- **状态**：Done
- 执行记录 1：`cargo-step` 重量解析移至填表前；全字段 `commit:true` 推进向导
- 执行记录 2：`fillInCargoEditor` 默认 commit false；`valuesEquivalent` 数值等价校验

### Step 11.36 重量单位按页面字段 + 填值生效 + 地址锁定

- **根因**：env 多 spec 合并含 "kg" 误换算 503→228.16；fill 无 value 校验；quote_session 跳过填址致 widget 回退
- **修复**：`readLiveWeightFieldMeta` 读激活字段单位；`fillLocatorWithVerify` + editor 断言；删 quote_session 跳过、强制点选候选
- **状态**：Done
- 执行记录 1：`cargo-weight.ts` / `cargo-field-fill.ts` 按页面 lb/kg 填原始值；`cargo-step` commit 前 `waitForCargoEditorFieldValues`
- 执行记录 2：`address-step` 始终 fillAddress + `widgetContainsConfirmedAddress`；单测 cargo-weight / address-step / cargo-step 更新

### Step 11.35 MotherShip 货物彻底重置 + 询价禁用 storageState

- **根因**：quote_session storageState / localStorage 残留货物；forceClear 无法删旧 freight 行
- **修复**：`cargo-reset.ts` 删行→ADD ITEM→填表→`assertWidgetCargoSummary`；询价 `freshCargoContext` 不复用 storageState
- **状态**：Done
- 执行记录 1：`removeAllFreightRows` / `clickAddFreightItem`；`openQuotePage({ freshCargoContext: true })` 清 storage
- 执行记录 2：`mothership.ts` 询价跳过 persistContext；摘要重量支持单托/总重双匹配


- **变更**：`prove-rpa-chain.ts` 支持 `PROOF_PAIR_ID=01|02|06` 切换地址与货物参数
- **状态**：Done
- 执行记录 1：新增 PROOF_PAIRS 映射；报告 JSON 含 proofPairId/label
- 执行记录 2：组 06 两次跑：QTE_0025 超时（旧 worker）；QTE_0026 货物重量闸门拦截 457 vs 2079Lbs

### Step 11.39 AxelSelectionBridge：search → placeId → place/{id} commit

- **根因**：`address-step.ts` `selectAddressSuggestion` 仅 DOM label 点选，input.value 更新但无 `placeId`，不触发 `GET place/{id}`
- **修复**：新增 `axel-selection-bridge.ts` 监听 search 响应绑定 placeId；`selectAddressWithPlaceBinding` 按 description 点选并等待 place 200；commit 以网络证据为准
- **状态**：Done
- 执行记录 1：`axel-selection-bridge.ts` + `address-suggestions.ts` `selectAddressWithPlaceBinding` + `address-step`/`address-commit` 接线
- 执行记录 2：单测 axel-selection-bridge 3/3、address-step 3/3；待实网 `record-dual-address-compare` 验证 placeApiHitCount>0

### Step 11.40 place commit 键盘主路径 + 闸门日志

- **根因（实网日志）**：`description-click` 命中 sc- 装饰 DOM，`placeNetworkLog=[]`，widget 未发 GET place
- **修复**：`selectAddressWithPlaceBinding` 改 keyboard-bound-index 主路径；pac/listbox/全键盘回退；`formatPlaceCommitFailure` 闸门日志
- **状态**：Done（待 post-fix 实网验证）
- 执行记录 1：debug-efb842 H2 CONFIRMED；H1/H3/H4/H5 REJECTED
- 执行记录 2：`keyboard-bound-{index}` 优先；失败输出原因分类（无请求/placeId 不匹配/HTTP 非 200/超时）

### Step 11.41 RPA_ADDRESS_MODE 隔离 + AddressAdapter 架构

- **根因**：MotherShip selection handler 未触发 GET place，无法验证 cargo→quote 下游
- **修复**：`RPA_ADDRESS_MODE=real|mock`；`AddressAdapter` 封装 DOM；commit 状态机 + 分类诊断日志
- **状态**：Done
- 执行记录 1：`lib/rpa/address-mode.ts` + `workers/rpa/address-adapter/*`；`address-step` 仅 `adapter.selectAddress()`
- 执行记录 2：单测 12/12；`prove-rpa-chain` 记录 `RPA_ADDRESS_MODE` 并校验 mock 日志

### Step 11.42 prove-rpa-chain 链路探通：跳过联想 API

- **根因**：MotherShip 联想 API 超时阻塞链路验证；PowerShell 未正确设置 env
- **修复**：`RPA_ADDRESS_MODE=mock` 或 `PROOF_SKIP_CANDIDATES=true` 时用 `proof-synthetic-candidates` 内置美国地址
- **状态**：Done
- 执行记录 1：`scripts/lib/proof-synthetic-candidates.ts`；prove 跳过联想直 POST quotes
- 执行记录 2：`.env` 默认 `RPA_ADDRESS_MODE=mock`；日志校验不再要求 candidates job

### Step 11.43 mock 链路探通：submit 闸门 + 快速填址

- **根因**：0029 超时因 Worker 未加载 mock；mock 仍调 ensureAddressSuggestions；submit 仍校验双地址锁定
- **修复**：mock 快速 fill 街道；fillCargo/submit/business-ready 跳过地址闸门；mock 后关闭 page 防 Browser 僵死
- **状态**：Done
- 执行记录 1：`mock-address-adapter` 改 mock-fast-fill；`cargo-step`/`thin-browser-driver`/`quote-business-ready` mock 放行
- 执行记录 2：Worker 重启后 0029 已跑通 address+cargo，待 submit 复测 prove-rpa-chain

### Step 11.45 移除 fixture 占位价，回归 real 真实报价

- **根因**：mock + fixture 注入固定四档价，与 MotherShip 无关，人工对账必失败
- **修复**：删除 `mock-chain-quote-fixture`；`.env` 改 `RPA_ADDRESS_MODE=real`；prove/demo 走联想 API
- **状态**：Done
- 执行记录 1：`quote-network-capture.ts` 移除 fixture 注入；无 axel/quote 时 pipeline 失败
- 执行记录 2：下一步修 real 模式 place commit，确保 Worker 日志含真实 axel/quote 非 mock-chain-fixture

### Step 11.49 place commit 修复：Google 事件触发 + React 注入

- **依据**：`docs/修复计划.md` R1-R7 + A1-A6 + B1-B2
- **输出**：`workers/rpa/fix-place/*`；`RPA_PLACE_FIX_MODE`；`mothership-address-adapter` 策略前置；`address-step` pickup 稳定验证
- **逆向**：`.rpa/recon/verdict.json` → `PLAN_B`（headed 探测时 google.maps 未加载）
- **状态**：Done
- 执行记录 1：新增 fix-place 目录（google-event/react-state/strategy/recon-helpers）+ `scripts/recon-place-trigger.ts` + env 开关
- 执行记录 2：`fix-place/browser-eval-scripts.ts` 纯字符串 evaluate，修复 `__name is not defined`；global-rules §18

### Step 11.52 Selection Trace 取证（不修交互）

- **目标**：验证 combobox 状态机 / aria-activedescendant / 人工真实点击目标，而非继续猜 ArrowDown
- **输出**：`workers/rpa/selection-trace.ts`；adapter events 含 `selection-trace:*`；`manual-selection-*.json` / `rpa-selection-trace.json`
- **状态**：Done
- 执行记录 1：候选出现后快照 activeElement、aria-activedescendant、pac、listbox；人工 click/keydown 监听
- 执行记录 2：单测 formatSelectionTraceEvent；待 manual vs rpa 对照 `combobox-dead` 与 `closestPacItem`

### Step 11.51 BLK-001 keyboard-bound ArrowDown 偏移

- **根因**：Google Places 默认高亮第 0 项；`keyboard-bound-0` 多按 1 次 ArrowDown 跳过唯一候选；`description-click` 先于键盘路径污染下拉状态
- **修复**：`arrowDownCountForPacIndex`；`alreadyHighlightedFirst` 默认 true；键盘路径优先于 DOM 点选；回退前 `ensureSuggestionDropdownOpen`
- **状态**：Done
- 执行记录 1：`address-suggestions.ts` keyboard-bound 主路径 + ArrowDown 计数修正
- 执行记录 2：单测 `arrowDownCountForPacIndex` 通过；待 `COMPARE_MODE=rpa` 组 99 验证 GET place/ChIJ… 200

### Step 11.53 BLK-001 styled-components 联想点击主路径

- **根因**：MotherShip 使用 sc-* DIV 联想非 Google pac；ArrowDown 误触 Use my location；键盘路径触发错误 placeId
- **修复**：`mothership-styled-suggestion-click.ts` 全页扫描+浏览器内 click；adapter 优先 styled 点击；禁用无 pac 时 keyboard-bound
- **状态**：Done
- 执行记录 1：新增 CLICK_MOTHERSHIP_STYLED_SUGGESTION、cookie dismiss、probe/demo-seattle-miami 脚本
- 执行记录 2：browser-place-commit 浏览器内 fetch 兜底；place-commit-strategy 接入


- **根因**：`fetchAxelPlaceLocation` 用 `page.evaluate(fetch)` → bridge 误判 `hasPlaceCommit`；`succeed()` 强制 `placeRequestObserved=true`；弱校验 `widgetContainsConfirmedAddress` 放行 pickup
- **修复**：prefetch 改 `context.request.get`；`assertWidgetLockedAfterCommit` + `isPickupReadyForDeliveryInput` 闸门；`settlePickupBeforeDelivery` 前 diagnostic 日志
- **状态**：Done
- 执行记录 1：`axel-place-prefetch.ts`/`mothership-address-adapter.ts`/`address-commit.ts`/`address-step.ts`
- 执行记录 2：单测 axel-place-prefetch/bridge 通过；pickup 未锁定将在 adapter 层 fail-fast，不再误进 delivery


- **根因**：prefetch 跳过 widget 双地址锁定；direct POST 仅 4 档且 lowest 偏低；demo 未传 quote_session_id
- **修复**：ensureDualAddressCommitted 交替 refill；orchestrator 仅 widget submit；demo 五档断言 + session
- **状态**：In progress
- 执行记录 1：isPickupReady 去掉 dropdown 误判；forceDismiss + chip 态空派送框；fillAddress 后 releasePickup
- 执行记录 2：enumerationMode 与 quote 路径分离；prefetch 后 Escape/Tab 锁定提货 chip

### Step 11.47 delivery 门牌绑定 + 枚举/query 对齐人工页

- **根因**：枚举首条即停；resolveSelection 用无门牌 display_label；direct quote 缺门牌 street
- **修复**：多 query 直到含门牌；bindingLabels 优先完整街道；axel 候选过滤门牌；候选排序
- **状态**：Done
- 执行记录 1：`enumerateAddressCandidates`/`ensureAddressSuggestions`/`enrichCandidateFromQuery`/`resolveSelection` 门牌优先
- 执行记录 2：`demo-screenshot-case-a` QTE_0051 delivery 已含 5678；lowest/bestValue 仍待 widget 双地址 commit + submit 路径

### Step 11.46 placeId 归一化 + axel-place-prefetch + direct axel/quote

- **根因**：复合 placeId 贪婪匹配；DOM 点选不触发 GET place；widget 双地址互斥
- **修复**：27 字符 ChIJ 截断；prefetch GET place；direct POST axel/quote；demo 实测两组不同价
- **状态**：Done
- 执行记录 1：`normalizeAxelPlaceId`/`axel-place-prefetch.ts`/`axel-quote-direct.ts`；orchestrator 双地址缓存走 direct
- 执行记录 2：demo QTE_0048/0049 source=rpa；Worker 日志 `direct axel/quote` 无 mock-chain-fixture

### Step 11.44 mock 全链路 PASS（已废弃 fixture 方案，见 11.45）

- **根因**：MySQL 3307 宕机致 quote-create 500；worker-rpa.log UTF-16 致 prove 日志正则失败；L2 缓存撞 hash
- **修复**：`dev:infra:start` 拉起 DB；prove 读日志 UTF-16 解码 + MySQL 探活
- **状态**：Superseded by 11.45
- 执行记录 1：QTE_0036 prove PASS 曾依赖 fixture，已撤销
- 执行记录 2：不得再使用 mock-chain-fixture 作为验收依据

### Step 11.48 RPA 联想键入纯街道优先（POI 粘连 label）

- **根因**：`address.street` 含 POI 全称导致 search 仅 1 条；`St`/`AvenueWest` 粘连使街道提取失败，联想下拉不出现
- **修复**：`extractStreetFromLabel` 粘连感知后缀；`resolveAddressAutocompleteInput` 替换 street；查询与 `ensureSuggestionDropdownOpen` 纯街道优先
- **状态**：Done
- 执行记录 1：`address-suggestions.ts` 导出 `extractStreetFromLabel`/`resolveAddressAutocompleteInput`；`buildAddressAutocompleteQueries` 全量基于纯街道
- 执行记录 2：`address-suggestions.test.ts` 新增粘连 POI/Western Avenue 用例；单测 26 项通过

### Step 11.54 直连 axel API 报价 CLI（抛弃 widget 交互）

- **根因**：Google Places / styled-components 下拉对自动化不可穿透，widget 状态机无法推进 commit
- **修复**：`lib/axel/*` Node fetch 客户端：search → place → quote；`npm run quote` CLI 输出 JSON
- **状态**：Done
- 执行记录 1：复用 `axel-selection-bridge` placeId 归一化；`buildAxelShipmentPayload` 与 HAR baseline 对齐
- 执行记录 2：`__tests__/lib/axel/quote.test.ts` 4 项通过；连续 3 次 CLI 同价验收

### Step 11.55 embed-demo 接入直连 axel 报价

- **根因**：embed-demo 仍走 Playwright widget，地址 commit 失败无法询价
- **修复**：`RPA_QUOTE_MODE=direct`（默认）时 candidates/quote 均走 `lib/axel`；Worker 无需开浏览器
- **状态**：Done
- 执行记录 1：`resolveAxelMothershipCandidates` + `fetchAxelQuoteItems` 接入 API 与 Worker
- 执行记录 2：`mothership-candidates.test.ts` 更新 direct/widget 分支单测

### Step 11.56 availableRates 兜底解析

- **根因**：部分路线 `data.rates.standard.lowest` 为空，仅 `availableRates` 有 customRate 定价
- **修复**：`decodeAxelQuoteBodyLenient` 在缺档时从 availableRates 按 serviceType/价格/时效补全
- **状态**：Done
- 执行记录 1：`extractTiersFromAvailableRates` + `carrierInfo.name` 承运人解析
- 执行记录 2：重跑 batch D/F/H；单测 availableRates fallback 通过

### Step 11.57 地址确认展示与不可选候选

- **根因**：已选择展示用 street/city/state 重组，与弹窗 display_label 不一致；部分 placeId GET place 失败仍可选
- **修复**：确认后展示 mothership_display_label；GET place 预校验 selectable；弹窗禁用不可选项
- **状态**：Done
- 执行记录 1：`formatAddressDisplayLabel`/`axelSearchRowToCandidate`/`verifyCandidates`
- 执行记录 2：`address-disambiguation-modal` 不可选提示；询价前二次校验 place

### Step 11.58 管理端客户加价配置（比例 / 固定金额）

- **需求**：管理端按客户设置卡派报价加价，支持运费百分比或固定 USD 金额
- **输出**：`markup_type`/`markup_fixed_amount` 迁移；`modules/pricing/markup-*`；`/admin/markup` + `/api/admin/markup-configs`
- **状态**：Done
- 执行记录 1：加价引擎 `getMarkupRule` 支持 percent/fixed；L2/RPA 链路按客户重算
- 执行记录 2：`admin-markup-routes` + `markup-validation` 单测通过；`.env.example` 增 `CUSTOMER_REGISTRY`

### Step 11.59 货物详情限制口径调整

- **需求**：托盘≤25；尺寸 L≤999/W≤99/H≤99 in；单托重≤9999 lb
- **输出**：`lib/constants/cargo-limits.ts`；`validation.ts`/`quote-form.tsx`；PRD §4.2.8 / domain-rules / 技术设计
- **状态**：Done
- 执行记录 1：整票总重硬顶改为 249,975 lb（9999×25）；校验增加单托重量上限
- 执行记录 2：`validation.test.ts` 边界用例 25 托/999×99×99 通过

### Step 11.60 Widget 地址 commit 金样本多层回退 + 网络闸门

- **根因**：styled-components 联想 sc-* 类名动态；DOM 点选未触发 GET place/{placeId}；verdict PLAN_B 仍走 google/react 注入
- **修复**：`gold-standard-targets` + `buildGoldDrivenLocators` 金样本文本/类名回退；`assertWidgetLockedAfterCommit` 要求 place 200 + 下拉收起；`place-commit-strategy` 按 verdict 跳过不可用路径；`address-step` 双地址闸门
- **状态**：Done
- 执行记录 1：`mothership-styled-suggestion-click.ts`/`mothership-address-adapter.ts`/`place-commit-strategy.ts`/`address-step.ts`/`axel-selection-bridge.ts`
- 执行记录 2：`probe:widget-address` 单测 17/17 通过；实网循环 `PROBE_WIDGET_LOOPS=3~10` + `prove:rpa-chain` 验收

### Step 11.61 Widget 填址前保留下拉 + Cookie 遮罩 + browser-place evaluate

- **根因**：`ensureAddressSuggestions` 成功后 `reopenStyledSuggestionDropdown` 清空输入致联想行消失；CookieYes revisit 抢焦点；`BROWSER_FETCH_AND_INJECT_PLACE` 为未调用 function 表达式 evaluate 返回 undefined；place 200 后 widget 未 chip 锁定即 fail
- **修复**：先 `waitForStyledSuggestionRows`/`ensureSuggestionDropdownOpen` 再点选；`fillAddress` 入口 `stabilizeQuoteLandingPage`；Cookie 遮罩 CSS 隐藏；place 200 后 `lockPickupIntoChip`；`pickupLostAfterDelivery` 改 widget DOM 校验；`widgetContainsConfirmedAddress` 标签无门牌号时不强制；`assertDualAddressStable` 修复 combined 未定义
- **状态**：Done
- 执行记录 1：`mothership-address-adapter.ts`/`page-prep.ts`/`browser-place-commit.ts`/`address-step.ts`
- 执行记录 2：`browser-place-commit.test.ts` 单测通过；pickup chip 锁定闸门补全

### Step 11.63 P0 实例拓扑 + 监听器指纹 + P2 fetch 劫持诊断

- **需求**：定位 place_changed 触发实例、listener 是否含 fetch、getPlace 完整性、未发 place 请求原因
- **输出**：`place-diagnostic-init.ts`/`place-diagnostic-probe.ts`/`scripts/probe-place-diagnostic.ts`；`RPA_PLACE_DIAG`；fix-place 阶段落盘
- **状态**：Done
- 执行记录 1：P0 包装 maps.event.addListener/trigger + instance.addListener；P2 fetch/XHR 劫持 axel/location/place
- 执行记录 2：`place-diagnostic.test.ts` 通过；`npm run probe:place-diagnostic` 可 headed 实网取证


- **根因**：旧方案仅 recon 脚本注入 Autocomplete hook；Playwright/evaluate 合成点击不触发 widget GET place；`event.trigger` 不保证 handler 执行
- **修复**：`cdp-click.ts`/`cdp-pac-commit.ts` Input.dispatchMouseEvent；`direct-listener-commit.ts` 直调捕获的 place_changed handler；`session-manager` 全局 `AUTOCOMPLETE_MONKEY_PATCH_INIT`；策略链 styled→cdp→direct-listener→browser-fetch→event→state
- **状态**：Done
- 执行记录 1：新增 fix-place/cdp-*、direct-listener-commit；recon-helpers 捕获 addListener；axel-place-prefetch 优先 CDP pac
- 执行记录 2：place-commit-strategy/cdp-click 单测 7/7 通过；待实网 `prove:rpa-chain` 验证 placeApiHitCount≥2

### Step 11.64 方案 A：双地址 vault 浏览器注入 + 随机地址报价探针

- **目标**：widget 路线通过 `addInitScript` 拦截 setState，保留 pickup；随机地址探针以拿到报价为成功标准
- **输出**：`workers/rpa/inject/dual-address-patch*`；`scripts/probe-random-address-quote.ts`；`RPA_DUAL_ADDRESS_PATCH`
- **状态**：Paused（待真人录证结论，禁止继续堆策略）
- 执行记录 1：`session-manager` 注入 init；`address-step` pickup/delivery 后 register+preserve
- 执行记录 2：probe 仍卡在 assertPickupCommitted；转入 Step 11.65 真人录证

### Step 11.65 真人完整链路录证（P0 冻结 fix-place）

- **冻结**：禁止新增 fix-place/*、place-commit-strategy、selection-trace、新回退/新点击
- **必做**：HAR + 录像 + Playwright Trace(CDP) + Console + Network；定位 GET place/{id} 200 瞬间
- **输出**：`scripts/record-human-commit-baseline.ts`；`docs/真人录证-commit时刻.md`
- **状态**：Done（脚本就绪；**5519e0ea 真人录证通过：2×place 200 + quote 200**）
- 执行记录 1：新增 human-commit-baseline 子模块与四检查点录证流
- 执行记录 2：`5519e0ea` 分析见 `docs/mothership-commit-瞬间分析.md`

### Step 11.66 真人 vs RPA 网络差分脚本

- **输入**：`HUMAN_RUN_ID=5519e0ea` + 同地址 RPA adapter 填址
- **输出**：`compare-human-rpa-network.ts` + `network-diff.ts`；`rpa-diff-{id}/diff-report.json`
- **状态**：Done
- 执行记录 1：`npm run compare:human-rpa-network` 对比 search→place 链
- 执行记录 2：单测 `human-rpa-network-diff.test.ts`；禁止新增 fix-place

### Step 11.67 差分实跑（5519e0ea 同址）

- **输入**：`HUMAN_RUN_ID=5519e0ea`；RPA 复用 `mothership-styled-click` 填址
- **输出**：`rpa-diff-7624e6b9/diff-report.json`；verdict=**MATCH**（place GET 200 各 2 次）
- **状态**：Done
- 执行记录 1：RPA pickup/delivery 均 `commitState=COMMITTED`；search→place 链 2/2 完整
- 执行记录 2：延迟差 ~9–10s（非阻塞）；未跑 quote（差分脚本仅填址）；下一步 `prove:rpa-chain` 同址全链

### Step 11.68 差分脚本观测体验修复

- **现象**：首址前长时间无反应；填派送时界面跳动；填址成功后浏览器闪退
- **根因**：`waitForQuoterWidgetHydrated` 无进度日志；`waitForDeliveryStreetInput` 循环点分区+Escape/Tab；脚本 `finally` 强制 `closeBrowser`
- **状态**：Done
- 执行记录 1：`address-side` 派送框快路径；`compare-human-rpa-network` 尊重 `shouldKeepBrowserOpen`+阶段日志
- 执行记录 2：headed 默认 Ctrl+C 结束，非闪退；`RPA_DWELL_MS` 可选停留

### Step 11.69 首址输入闪清修复

- **现象**：填第一个地址时界面已出现文案后突然清空
- **根因**：`triggerAddressAutocomplete`/`ensureSuggestionDropdownOpen` 无条件 `fill("")`；键入后 `scrollQuoteWidgetIntoView({force:true})` 扰动 widget
- **状态**：Done
- 执行记录 1：`addressInputMatchesQuery` 跳过冗余清空；滚动提前到键入前
- 执行记录 2：`openAddressSide` 返回同一 Locator，避免二次解析失焦

### Step 11.70 派送填址加速 + 可视全链脚本

- **目标**：第二地址 search→place ~2s；可视验收地址→货物→报价
- **状态**：Done
- 执行记录 1：`deliveryFast` 缩短 styled 等待 + `legacy-label-row-fast`；`visual-quote-full-chain.ts`
- 执行记录 2：`RPA_FAST_CLICK`/`RPA_SLOW_MO_MS=0`；`npm run visual:quote-full-chain`

### Step 11.72 视觉优先双地址完成 → 直接填货物

- **需求**：不等 place 网络/下拉收起；widget 或地图已见双地址即进入 Freight details
- **修复**：`isAddressesVisuallyCompleteForCargo`；`fillAddressStep`/`cargo-lock` 视觉放行
- **状态**：Done
- 执行记录 1：`evaluateAddressesVisuallyComplete` 双州码/门牌/Freight details；delivery adapter 失败仍可走货物步
- 执行记录 2：`address-commit.test.ts` 截图态用例；`settleAddressFieldsForCargo` 视觉短路

- **现象**：从上到下扫 DOM；Freight details 弹出但不填值；脚本结束闪退
- **根因**：`removeAllFreightRows`/`tryLocatorStrategies` 循环点选；弹层 input 在 widget 外
- **状态**：Done
- 执行记录 1：`fillLandingFreightPopover` 直填 How many pallets/Length/Weight；`legacyOnly` 单次点联想
- 执行记录 2：派送侧去掉 `waitForDeliveryStreetInput` 循环；`landingPopoverMatchesExpected` 放行 commit

### Step 11.73 sign-up-quote 报价页 lenient 解析

- **需求**：报价页已出现；axel/quote 有数据但 rates.* 叶节点不全，禁止 assertQuoteContract 硬拦
- **修复**：`decodeQuotePayloads` 契约失败时走 `decodeAxelQuoteBodyLenient`（availableRates 推断档位）
- **状态**：Done
- 执行记录 1：`tryLenientAxelDecode` 识别 sign-up-quote + axel/quote URL；source=lenient
- 执行记录 2：`quote-capture.test.ts` 契约不全 lenient 用例；移除 assertQuoteContract 抛错路径

### Step 11.74 可视冲刺模式 RPA_VISUAL_RUSH

- **需求**：填址点选联想 + 填货物后直接点报价，不等待锁定/契约/后端检测
- **修复**：`isRpaVisualRushMode`；address/cargo/submit/pipeline 跳过闸门；关闭 dual-address-patch
- **状态**：Done
- 执行记录 1：`visual-quote-full-chain` 默认 `RPA_VISUAL_RUSH=true`；`submitQuoteForm` 直点 Get a freight quote
- 执行记录 2：pickup/delivery 均取消 `place`/`legacy` 长等待；联想点击后直接继续

### Step 11.75 Direct 优先 / Widget 兜底报价策略

- **需求**：Direct 为主路径；仅 Direct 明确失败后才允许 Widget；禁止并行/预热/默认 Widget
- **修复**：`quote-strategy.ts` + `orchestrator`/`mothership` 串联；`pipeline` 闸门 `widgetFallbackAfterDirectFailure`
- **状态**：Done
- 执行记录 1：node `fetchAxelQuoteItems` → 浏览器 kernel `captureQuotesDirectFirst`；`RPA_DISABLE_WIDGET_QUOTE_FALLBACK` 可关 fallback
- 执行记录 2：`quote-strategy.test.ts` + pipeline gate；脚本与 `.env` 移除 `RPA_QUOTE_MODE=widget` 默认

### Step 11.76 人工 docx 系统全链路探针

- **需求**：不经 RPA 代码直调，走 POST /api/quotes 系统链路验证 docx 10 组
- **输出**：`scripts/probe-human-doc-system.ts` + `npm run probe:human-doc-system`
- **状态**：Done
- 执行记录 1：`scripts/lib/human-doc-10pairs.ts` 共享地址；候选→创建→轮询 GET
- 执行记录 2：H01 系统探针 PASS（4 档 rpa）；全量 10 组后台跑 `.rpa/human-doc-system-probe/`

### Step 11.77 系统探针失败组地址误匹配修复

- **现象**：H06/H07 Bridge 误点；H08 city-only；H09/H10 队列饥饿
- **修复**：`suggestionLabelConflicts` + 禁用 preknown 视觉放行 + 键盘索引优先 + 探针候选打分
- **状态**：Done
- 执行记录 1：`mothership-styled-suggestion-click.ts` 拒绝 Bridge/sc-fallback；`address-commit` 桥接校验
- 执行记录 2：`mothership-address-adapter` preknown-keyboard；探针 `pickBestCandidate` + 轮询 600s

### Step 11.78 系统探针失败组修复验证

- **现象**：H06-H10 place_id_mismatch（Bridge 误点）；preknown synthetic 被闸门拦截
- **修复**：preknown 跳过 legacy 点选 + synthetic 放宽闸门 + dev 重启加载新代码
- **状态**：Done
- 执行记录 1：`npm run probe:human-doc-system` 10/10 PASS（`system-e2583d51.json`）
- 执行记录 2：探针 `uniqueWeight` 防 cache 空档；H06/H07 单测 12–17s 通过

### Step 11.79 代码库清理（冗余文件与过时文档）

- **目标**：合并重复数据、删除废弃 re-export 与一次性 debug 脚本、更新项目索引
- **状态**：Done
- 执行记录 1：删除 `docs/修复计划.md`、20 个 `debug-*.ts`、`browser-eval-scripts.ts`、`event-capture.ts`
- 执行记录 2：共享 `human-doc-10pairs`/`pick-mothership-candidate`；更新 `查价项目.md` 与 `verify-address-candidates`

### Step 11.80 availableRates 补全缺失档位（非仅缺 lowest 时）

- **根因**：`decodeAxelQuoteBodyLenient` 在 `rates.*` 已有 standard/lowest 时跳过 `availableRates`，导致 standard.bestValue 等扩展档丢失（2+1 vs 人工 3+1）
- **修复**：`mergePartials` 始终合并 `availableRates` 中缺失档，不覆盖已有 `rates.*` 叶节点 finalPrice
- **状态**：Done
- 执行记录 1：`payload-decode.ts` `extractFromContractBodies` + `decodeAxelQuoteBodyLenient`
- 执行记录 2：单测「rates 已有 lowest 仍补全 bestValue」通过

### Step 11.81 禁止 availableRates customRate 伪造 UI 档位名

- **根因**：Step 11.80 无条件合并 `availableRates`；customRate 池按价/时效推断 lowest/fastest/bestValue，在 `rates.*` 已有 3+1 时膨胀为 6 档且名称与价格错位（QTE_20260626_0045）
- **修复**：档位名仅以 `rates.{standard|guaranteed}` 下子键为准；customRate 推断仅当该 serviceLevel 块无 UI 子键；显式 serviceType 亦受槽位白名单约束
- **状态**：Done
- 执行记录 1：`listUiRateOptionsFromRatesBlock` + `extractTiersFromAvailableRates(body, rates)`
- 执行记录 2：单测「3+1 禁止伪 guaranteed/lowest|fastest」通过

### Step 11.82 fastest 档位 id 关联 + 显式 serviceType 补档

- **根因**：`rates.standard.fastest` 常为 `{ id }` 引用无 finalPrice；或最快档仅在 `availableRates` 显式 `serviceType: fastest`；旧逻辑只读叶节点导致缺 $768.88
- **修复**：`resolveTierFromRatesSlot` 经 id 查 `availableRates`；standard 允许显式 serviceType 补缺失档；guaranteed 仍仅 `rates.guaranteed` 子键
- **状态**：Done
- 执行记录 1：`extractTiersFromRates(rates, availableRates)` + `listAllowedRateOptionsForSupplement`
- 执行记录 2：单测 id 引用 fastest / 显式 fastest 补档通过

### Step 11.83 MotherShip 会话自动 bootstrap

- **需求**：缺 `.rpa/mothership-storage.json` 时不要求用户手动 `record:mothership`
- **修复**：`ensureMothershipStorageState` headless 访问 `MOTHERSHIP_QUOTE_URLS` 并 `persistContext`；worker 启动与 axel 直连前自动调用
- **状态**：Done
- 执行记录 1：新增 `lib/rpa/ensure-storage-state.ts`；`AxelHttpClient.create()` + worker bootstrap
- 执行记录 2：单测 ensure-storage-state；`RPA_AUTO_BOOTSTRAP_STORAGE=false` 可关

### Step 11.84 Priority1 FTL 人机协作录制

- **需求**：FTL 整车自动填至卡住，人工补全并记录操作直至报价
- **输出**：`human-assist-recorder.ts`、`runPriority1HumanAssistChain`、`npm run visual:priority1-human-assist`
- **状态**：Done
- 执行记录 1：Step 2 软自动（失败 warn 不 throw）+ DOM 事件录制 + 报价轮询保存 `.rpa/recordings/priority1-human-*.json/js`
- 执行记录 2：人机模式强制 headed；CLI 默认 F01-ftl-dry-van-full

### Step 11.85 Priority1 全矩阵 benchmark 失败项修复

- **根因**：Freight Class 观测读到错误 select；Open Deck evaluate `__name`；FTL/Expedited Feathery checkbox headless 无效
- **修复**：`selectFreightClass` 优先 `#freight-type`；evaluate 改 function 声明；`forceFeatheryCheckbox`；Expedited cardText 放宽
- **状态**：Done
- 执行记录 1：`form-interactions.ts` + `visual-priority1-full-chain.ts` + `shipment-types.ts`
- 执行记录 2：单测 freightClassOptionMatches；复跑 `benchmark:priority1-canonical-matrix`

### Step 11.86 P08 LTL 全附加服务人工录制

- **需求**：记录人工勾选 11 项 LTL 附加服务并提交至报价
- **输出**：`runPriority1P08AccessorialRecording`、`npm run record:priority1-p08-accessorials`
- **状态**：Done
- 执行记录 1：`P08_LTL_ACCESSORIAL_LABELS` + `humanAssistHandoff: ltlAccessorials` 跳过自动勾选
- 执行记录 2：2026-07-01 人工实机 11 项全勾并出报价（`quoteDetected: true`）；P08 `verified: recorded`

### Step 11.86b P08 录制器事件捕获修复

- **需求**：纯人工模式保存时 `events` 为空导致无法回放 11 项勾选
- **修复**：`resolveInteractive` 向上解析 label/checkbox；轮询累积事件；出报价且无事件时用清单生成回放脚本
- **状态**：Done
- 执行记录 1：`human-assist-recorder.browser.js` + `run-manual-recording.ts` 累积与 fallback
- 执行记录 2：回填 `priority1-P08-ltl-all-accessorials-manual-2026-07-01T0553.json/js`

### Step 11.86c P08 自动检测通过

- **需求**：benchmark / canonical inspect 自动勾选 11 项 LTL 附加并出报价
- **修复**：`toggleLtlAccessorial` 对齐人工录制（getByLabel + force + DOM 校验）；`isLtlAccessorialChecked`
- **状态**：Done
- 执行记录 1：`form-interactions.ts` 重写附加勾选与勾选态读取
- 执行记录 2：单测 `ltlAccessorialLabelMatches`；`PRIORITY1_MATRIX_FILTER=P08` 验证 batch=d3e9b973 11/11 勾选 + 4 条报价

### Step 11.88 FTL 第二类卡住修复 + 人机录制入口

- **根因**：`selectFtlAdditionalService` 对隐藏 `#cb_as_ftl*` 调用 `locator.isChecked()`，Playwright 默认 90s×5 次重试 → 看似卡死
- **修复**：`isFeatheryCheckboxChecked` + `isFeatheryCardSelected` 替代 isChecked；`npm run record:priority1-ftl-path`
- **状态**：Done
- 执行记录 1：`form-interactions.ts` FTL 附加服务勾选与观测
- 执行记录 2：`record-priority1-ftl-path.ts` 自动填至 handoff + 人工节点录制

### Step 11.89 F01 FTL 人工录制 handoff

- **需求**：Dry Van 后 Full FTL 自动点不到，改人工录制 Additional Services 及后续
- **输出**：`humanAssistHandoff: ftlAdditionalServices` + `runPriority1FtlPathRecording`
- **状态**：Done
- 执行记录 1：`fillStep2Ftl` 跳过 Additional Services；`printFtlAdditionalHandoff` 清单
- 执行记录 2：`runHumanAssistPhase` Ctrl+C/浏览器关闭保存；`npm run record:priority1-ftl-path`

### Step 11.90 FTL 日历报价检测 + F01 人工录制验收

- **需求**：FTL 报价为日历表格无 `$`；F01 人工录制 Full FTL 节点
- **输出**：`parseFtlCalendarQuotes`；`priority1-F01-ftl-dry-van-full-human-2026-07-01T0715.json`
- **状态**：Done
- 执行记录 1：`quote-extract.ts` FTL 日历页识别；录制 40+ DOM 事件含 Full FTL
- 执行记录 2：`selectFtlAdditionalService` 优先 `getByText(label, exact)` 对齐人工录制

### Step 11.91 F04 Open Deck Type #PickupLocation3

- **需求**：F04 人工录制；批量 F04–F07 卡在 Open Deck Type 未找到选项
- **输出**：`OPEN_DECK_TYPE_SELECT_SELECTOR`；`priority1-F04-ftl-open-deck-flatbed-human-2026-07-01T0747.json`
- **状态**：Done
- 执行记录 1：根因 `EXCLUDED_SELECT_ID_RE` 误排除 `#PickupLocation3`；优先 selectOption 该节点
- 执行记录 2：含 deck 选项的 select 不再被 PickupLocation 规则跳过；F04 `verified: recorded`

### Step 11.92 无即时报价 → 人工跟进终态

- **需求**：F08–F11 等提交后出现 MORE INFORMATION NEEDED，不应记为失败
- **输出**：`isPriority1ManualFollowupPage`；`quoteOutcome: manual_followup`；标准文案
- **状态**：Done
- 执行记录 1：`quote-extract.ts` 识别人工跟进页；`message` 返回「表单已提交，站点判定需人工确认细节后报价」
- 执行记录 2：`visual-priority1-full-chain` + benchmark `manual_followup` 计为成功

### Step 11.93 全矩阵 24 条系统联调清单

- **需求**：确认 24 条 canonical 路线供 Priority1 模块系统测试
- **输出**：`buildPriority1CanonicalTestCases`；`export:priority1-canonical-test-matrix`
- **状态**：Done
- 执行记录 1：24 条全部 `verified: recorded`；预期终态 instant_quote / manual_followup 映射
- 执行记录 2：产出 `.rpa/priority1-canonical-test-matrix.json` 含 POST /api/priority1/quotes 载荷

### Step 11.94 填写规则与重量硬限制

- **需求**：明确互斥/重量上限；E03 10000 lb、F11 内嵌 Expedited 修正
- **输出**：`form-constraints.ts`；`.rpa/priority1-form-constraints.md`
- **状态**：Done
- 执行记录 1：`PRIORITY1_WEIGHT_LIMITS` Expedited Large ≤10000；Liftgate 仅 Large Straight
- 执行记录 2：benchmark/export 前置 `validatePriority1FormConstraints` 校验

### Step 11.95 Priority1 系统查询链路接入生产

- **需求**：24 条 canonical 跑通后，人工可在系统内提交 Priority1 询价并拿到即时报价或人工跟进终态
- **输出**：`run-quote.ts` 接入 `visual-priority1-full-chain`；job-handler `manual_followup`；serializer + UI
- **状态**：Done
- 执行记录 1：worker `runPriority1QuoteRpa` 非 mock 时调用可视全链 RPA；`PRIORITY1_MANUAL_FOLLOWUP` 记为 done
- 执行记录 2：`Priority1QuoteWidget` + `QuoteResultPanel` 展示人工跟进；API 校验叠加 `form-constraints`

### Step 12.0 Priority1 服务端日期校验与中文报错

- **需求**：修复 p1DateToIso client/server 边界错误；用户可见报错中文化
- **输出**：`date-format.ts`；`api-errors.ts`；route/widget 使用 `resolvePriority1UserMessage`
- **状态**：Done
- 执行记录 1：`pickup-date.ts` 改引服务端安全的 `date-format`，不再经 `ui-labels`（use client）
- 执行记录 2：API/前端将英文框架异常转为中文提示，保留业务校验原文

### Step 11.99 Priority1 询价处理失败修复

- **需求**：POST /api/priority1/quotes 返回「Priority1 询价处理失败」
- **输出**：orchestrator 入队失败可读错误；markQuoteStarted 改 safeRecord；quote_id 冲突重试
- **状态**：Done
- 执行记录 1：移除提交前 await markQuoteStarted（Redis 异常不再阻断入队前流程）
- 执行记录 2：入队失败标记 quote failed + API 返回 Redis/队列具体原因

### Step 11.98 Priority1 报价界面中文化与数据修正

- **需求**：中文展示、移除 Cabo TMS 按钮、补全日期、官网金额、提货日仅可选今天之后
- **输出**：`P1_RESULT_UI`；DOM 日期解析；`pickup-date.ts`；优先可见卡片金额
- **状态**：Done
- 执行记录 1：`extractPriority1VisibleQuotes` 优先 QUOTE 卡片 DOM；展示 `totalUsd` 官网价
- 执行记录 2：`P1DateField` min=明天；校验「提货日须晚于今天」

### Step 11.99 Priority1 承运商与金额解析修正

- **需求**：页面显示「未解析承运商」、金额与官网不一致
- **输出**：`quote-extract.ts` Feathery 摘要优先；按卡片顺序对齐 rank；enrich 回填承运商
- **状态**：Done
- 执行记录 1：`mergeLtlSummaryWithDomCards` 以摘要金额/承运商为准，DOM 仅补日期时效
- 执行记录 2：`enrichDomCardsFromNetwork` 未解析承运商时按序号从摘要回填；增强 DOM 承运商正则

### Step 11.100 Priority1 FTL 日历日期与金额落格修正

- **需求**：FTL 结果页显示 `Invalid Date`，且日历金额/日期位置与官网不一致
- **输出**：`calendar-grid.ts` 兼容 `MM/DD/YYYY`；`parseFtlCalendarQuotes` 提取实际日历日；按真实日期落格
- **状态**：Done
- 执行记录 1：`buildPriority1CalendarGrid` 新增日期解析与 `DAY:n` 落格，修复空日历/Invalid Date
- 执行记录 2：`priority1-quote-extract.test.ts` 与 `calendar-grid.test.ts` 覆盖 FTL 日历解析与渲染

### Step 11.97 Priority1 500lb 无报价与 RPA 提速

- **需求**：改重量后「暂无报价」；询价耗时过长
- **输出**：`waitForQuotes` 去除壳子就绪早退；`legacy-tier` 兼容；workerFast 跳过截图/停留
- **状态**：Done
- 执行记录 1：`resolvePriority1StoredQuotes` 兼容旧 tier；报价轮询仅在有金额时结束
- 执行记录 2：worker `RPA_DWELL_MS=0` + `workerFast`；前端 5s 后 600ms 轮询

### Step 11.96 Priority1 官网风格报价展示

- **需求**：Priority1 结果不再复用 MotherShip `QuoteResultPanel`；LTL 卡片列表 + FTL 日历两种界面与官网一致
- **输出**：`quote-storage.ts`；`priority1-ltl-quote-options`；`priority1-ftl-schedule-calendar`；`priority1-quote-result-panel`；serializer `priority1` 字段
- **状态**：Done
- 执行记录 1：worker `quotesJson` 存 `provider:priority1` 完整行；GET 报价返回 `priority1.display_mode` 与加价后 `lines`
- 执行记录 2：`Priority1QuoteWidget` 接入专用结果面板；FTL 全宽日历 + 侧边运单摘要

### Step 11.99 管理端客户管理与 DB API Key

- **需求**：多客户无需改 .env 即可新增租户并签发 Key；加价按客户隔离
- **输出**：`host_customer`/`service_api_key` 表；`/admin/customers`；`verifyServiceTokenAsync`
- **状态**：Done
- 执行记录 1：管理端创建客户自动生成 `chj_` Key；env `HOST_SERVICE_TOKENS` 继续兼容
- 执行记录 2：middleware env 快路径 + 路由 DB 校验；加价列表合并 DB 客户

### Step 11.98 加价比例支持 0.01% 步长

- **需求**：加价配置百分比需支持 0.01% 精度
- **输出**：`markup-validation.ts`；`prisma` DECIMAL(5,2) 迁移；管理端输入 step=0.01；`formatPercentDisplay`
- **状态**：Done
- 执行记录 1：`parsePercentValue` 改为两位小数；`markup_config`/`quote_record.markup_percent` 扩精度
- 执行记录 2：管理端与报价 breakdown 展示支持 0.01%；单测覆盖 0.01% 加价计算

### Step 11.97 MotherShip 报价序列化误走 Priority1 legacy 修复

- **需求**：服务器 MotherShip 询价成功但前端显示「暂无报价数据」
- **输出**：`modules/priority1/quote-storage.ts`；`quote-serializer-priority1` 单测
- **状态**：Done
- 执行记录 1：`resolvePriority1StoredQuotes` 仅在 `pickupJson.priority1_shipment_type` 存在时才做 legacy-tier 转换
- 执行记录 2：MotherShip tier 数组恢复序列化为 `quotes` 字段；线上 host/submit 与 embed `QuoteCard` 可正常展示

### Step 11.87 查价项目.md 同步（部署 + 宿主 API + 公制取整）

- **需求**：系统经宝塔部署、宿主公开 API、Direct 生产配置、axel 整数边界后，更新新人入口文档
- **输出**：`docs/查价项目.md` v1.3（方式 A/B、BLK-012~014、ADR-6/7、宝塔部署、公制样例）
- **状态**：Done
- 执行记录 1：更新 AI 交接、关键 API、环境变量、排障与 Docker 双部署路径
- 执行记录 2：补充 `toAxelWhole*` 精度边界说明与 `chajia-neibu-2026` 内网 token

---

## Phase 12 — 报价提速与 API 稳定性

> 改动清单见 `docs/报价提速与稳定性-改动清单.md`。按 P0 → P1 → P2 → P3 顺序执行。

### Step 12.1 报价去重 fetchPlace + 并行 resolve

- **输入**：P1-1；`lib/axel/quote-from-request.ts`
- **输出**：每侧 place 最多 1 次 HTTP；pickup/delivery `Promise.all`
- **验证**：单测 + `probe:human-doc-system` 10/10
- **状态**：Done
- 执行记录 1：`resolveSide` 改为返回已解析 `place`，已知 `placeId` 与 search 命中路径均只 `fetchPlace` 1 次；pickup/delivery 改并行解析
- 执行记录 2：新增 `__tests__/lib/axel/quote.test.ts` 调用次数断言；`npm run lint` 与 `npx vitest run __tests__/lib/axel/quote.test.ts` 通过

### Step 12.2 候选 verify Top-K

- **输入**：P1-2；`lib/axel/candidates.ts` + env `AXEL_CANDIDATE_VERIFY_TOP_K`
- **验证**：候选 API 耗时下降；至少 1 条 selectable
- **状态**：Done
- 执行记录 1：`getAxelCandidateVerifyTopK()` 默认 3；`verifyCandidates` 仅前 K 条 `fetchPlace`，其余 `selectable: true`
- 执行记录 2：`candidates.test.ts` 断言 fetchPlace 次数；`deploy/.env.bt-host-mysql.example` 增 `AXEL_CANDIDATE_VERIFY_TOP_K=3`

### Step 12.3 host submit 轮询 500ms

- **输入**：P1-3；`modules/host/wait-for-quote.ts`
- **验证**：submit 完成后 <1s 返回
- **状态**：Done
- 执行记录 1：`getHostQuotePollIntervalMs()` 默认 500ms；`waitForQuoteDetail` 不再使用 embed 的 2s 间隔
- 执行记录 2：`wait-for-quote.test.ts` 覆盖 env 解析；模板增 `HOST_QUOTE_POLL_INTERVAL_MS=500`

### Step 12.4 地址候选 Redis 短缓存

- **输入**：P1-4；TTL 120s
- **验证**：同址二次候选 <500ms
- **状态**：Done
- 执行记录 1：`addr-cand:{md5}` 键；`mothership-candidates` 读写缓存；Redis 失败时静默降级
- 执行记录 2：`candidates-cache.test.ts` + `mothership-candidates.test.ts` 缓存命中用例；`cache.test.ts` TTL 120s

### Step 12.5 inline direct quote（API 快乐路径）

- **输入**：P2-1；`orchestrator` + `RPA_INLINE_DIRECT_QUOTE`
- **验证**：`inline-direct 成功`；无 queue 等待；10/10
- **状态**：Done
- 执行记录 1：`submitQuote` 在 L2 miss 后优先尝试 inline `fetchAxelQuoteItems`；成功直接写 L2/L3 + `quoteRecord.done` + L1 幂等
- 执行记录 2：失败仅 warn 并无损回退 BullMQ；`orchestrator.test.ts` 覆盖 success/fallback，`RPA_INLINE_DIRECT_QUOTE=true` 写入部署模板

### Step 12.6 host submit 同步 done 免轮询

- **输入**：P2-2；`public-orchestrator.ts`
- **验证**：inline 成功时 0 次 DB 轮询
- **状态**：Done
- 执行记录 1：`hostPublicSubmitAndWait` 在 `submitQuote().status === done` 时直接 `findFirst + serializeQuoteDetail`，不再走 `waitForQuoteDetail`
- 执行记录 2：`public-orchestrator.test.ts` 新增直返路径断言；`npm run lint` 与 host 聚焦测试通过

### Step 12.9 解码器按 MotherShip 实际档位展示（取消 standard/lowest 硬门槛）

- **根因**：部分路线 axel/quote 仅返回 `rates.*.bestValue`，`hasMinimumTiers` / `normalizeQuoteItems` 强制 `standard/lowest` 导致整单 `RPA_DATA_INVALID`
- **验证**：Denver→Boston 仅 bestValue 三档可解码、入库、展示；`quote-capture` / `quote-completeness` 单测通过
- **状态**：Done
- 执行记录 1：`payload-decode` / `quote-schema-validator` / `quote-completeness` 改为「至少 1 条有效运价」即通过
- 执行记录 2：新增 Denver→Boston bestValue-only 用例；`mothership.validate` 放宽单档与缺 guaranteed 场景

### Step 12.7 Priority1 分队列

- **输入**：P3-1；独立队列/worker
- **验证**：生产 MotherShip 不被 Priority1 阻塞
- **状态**：待执行

### Step 12.8 HTTP worker 并发（可选）

- **输入**：P3-2
- **验证**：3 并发报价成功率不降
- **状态**：待执行

### Step 12.10 管理端预警增强与查价记录

- **输入**：预警详情需中文根因、客户/地址/货物；取消 RPA 状态与指标看板；队列监控改为查价流水（最多 1000 条）
- **验证**：预警详情无 JSON 根因；查价记录 API 分页；导航仅保留查价记录
- **状态**：Done
- 执行记录 1：新增 `quote_query_log` 表与 `query-log` 模块；全链路写入 outcome；`alert-presentation` 中文根因
- 执行记录 2：重写 `/admin/alerts` 详情与 `/admin/queues` 查价记录页；移除 RPA/指标看板导航

### Step 12.12 管理端与嵌入演示登录门控

- **输入**：管理端不再预填/展示演示账号；嵌入演示须客户 ID + 密码登录后按客户查价
- **验证**：`/login` 空表单；`/embed-demo` 登录 CUST_002 可查价；Cookie 会话可调用 host API
- **状态**：Done
- 执行记录 1：`embed-demo` JWT Cookie + middleware 无 Bearer 时注入 `x-customer-id`；`EmbeddedQuoteWidget` 支持无 Token
- 执行记录 2：管理端 `login-client` 清空默认值；seed 密码改为 `kj123456`；`auth.test.ts` 覆盖演示会话

### Step 12.13 管理员密码同步 + 嵌入演示去掉加价展示

- **根因**：`seed.ts` upsert `update: {}` 不更新已有 `admin_demo` 密码；嵌入演示页展示加价易误解
- **验证**：`prisma db seed` 后 `admin_demo/kj123456` 可登录；`/embed-demo` 仅显示客户编号
- **状态**：Done
- 执行记录 1：`seed` 更新 `passwordHash`；`install-bt-host-mysql.sh` 部署后自动 `db seed`；`chajia.sql` 同步新哈希
- 执行记录 2：移除 embed-demo 加价配置文案；服务器可单独执行 `npx prisma db seed` 立即改密

### Step 12.14 登录不保存账号密码

- **输入**：管理端与嵌入演示每次进入须手动填写账号密码；禁止浏览器记住
- **验证**：刷新 `/embed-demo` 须重新登录；关闭浏览器后管理端须重新登录
- **状态**：Done
- 执行记录 1：登录表单 `autoComplete=off`；管理端 Token 改 `sessionStorage` 并清理旧 `localStorage`
- 执行记录 2：嵌入演示进入页清除 Cookie、取消自动恢复会话；演示 Cookie 改为浏览器会话级

### Step 12.15 客户管理设置嵌入演示密码

- **输入**：管理端按客户设置 /embed-demo 登录密码；嵌入演示按客户校验
- **验证**：客户管理可设置密码；CUST_002 改密后旧密码失效
- **状态**：Done
- 执行记录 1：`host_customer.embed_password_hash` 迁移；`verifyCustomerEmbedPassword` 按客户 bcrypt 校验
- 执行记录 2：客户管理「设置密码」与新建时密码字段；仅 .env 客户仍走全局 `EMBED_DEMO_PASSWORD`
