You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1659 lines
95 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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(119120) + 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 — 环境与底座
> Task072 → 073 → **119 → 120** → 070 → 071 → 097 → 030
> 状态:**072030、119120 已完成****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.example6 项必填变量、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
- 执行记录 1lib/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
- 执行记录 1rpa-worker 透传 MOTHERSHIP_QUOTE_URLS/RPA_* envrpa_state volume 挂载 /app/.rpa
- 执行记录 2docker compose config 校验通过;.gitignore 忽略 .rpa/
### Step 0.5 Prisma Schema
- **Task**task-070
- **输出**`prisma/schema.prisma`7 实体)
- **验证**`npx prisma validate` 零错误
- **状态**Done
- 执行记录 17 模型 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
- 执行记录 1migration `20260616000000_init` + markup CHECK 约束seed admin_demo/Demo@123
- 执行记录 2migrate deploy + seed 已通过MySQL 宿主机端口改为 3307 避开本机 3306 冲突)
### Step 0.7 Prisma 单例
- **Task**task-097
- **输出**`lib/prisma.ts`
- **验证**`prisma.$connect()` 成功
- **状态**Done
- 执行记录 1lib/prisma.ts 全局单例dev 热重载防多实例
- 执行记录 2tsc 编译通过;$connect 待 MySQL 就绪后验证
### Step 0.8 Redis 单例
- **Task**task-030
- **输出**`lib/redis.ts`
- **验证**`getRedis().ping()` → PONG
- **状态**Done
- 执行记录 1lib/redis.ts getRedis() 单例 + REDIS_URL 缺失抛错 + error 日志
- 执行记录 2tsc 编译通过ping 待 Redis 容器启动后验证
---
## Phase 1 — 基础设施与缓存
> Task096 → 031 → 032 → 033 → 034 → 035 → 035-ip → 036
### Step 1.1 统一响应包络
- **Task**task-096
- **输出**`lib/response.ts`ok/fail
- **验证**Vitest 覆盖 code/message/data
- **状态**Done
- 执行记录 1lib/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
- 执行记录 1setL1 SETNX 不覆盖getL1 JSON 反序列化
- 执行记录 2单测验证 TTL=86400
### Step 1.3 L2 热缓存
- **Task**task-032
- **输出**getL2/setL2
- **验证**TTL 180~210s jitter
- **状态**Done
- 执行记录 1setL2 写入 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
- 执行记录 1setL3 写入 stale:{cargo_hash}TTL 1800s
- 执行记录 2单测验证 TTL=1800
### Step 1.5 缓存击穿锁
- **Task**task-034
- **输出**acquireLock/releaseLock/waitAndGetL2
- **验证**SETNX lock:quote:{hash} TTL 5s
- **状态**Done
- 执行记录 1acquireLock SETNX + releaseLock DEL + waitAndGetL2 2s
- 执行记录 2单测验证重复 acquire 返回 false
### Step 1.6 客户限流
- **Task**task-035
- **输出**`modules/cache/rate-limiter.ts`
- **验证**60 次/分钟/customer
- **状态**Done
- 执行记录 1checkRateLimit 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
- 执行记录 1checkIpRateLimit ban:ip:{ip} + ratelimit:ip:{ip}
- 执行记录 2单测 1000 通过 + 超限 banned=true
### Step 1.8 RPA 熔断
- **Task**task-036
- **输出**`modules/cache/circuit-breaker.ts`
- **验证**:连续 3 次失败 openTTL 600s
- **状态**Done
- 执行记录 1isCircuitOpen/openCircuit/closeCircuit/recordRpaFailure
- 执行记录 2单测 3 次失败熔断 + closeCircuit 清除
---
## Phase 2 — 鉴权
> Task001 → 002 → 003 → 004 → 005 → 098
### Step 2.1 JWT 签发/校验
- **Task**task-001 ~ task-003
- **输出**`modules/auth/`(宿主 token + 管理员 JWT
- **验证**:有效/过期/伪造 token 分支
- **状态**Done
- 执行记录 1jwt.ts sign/verify/decode + login route + middleware 路由保护
- 执行记录 2jwt.test.ts 4 用例通过tsc 零错误
### Step 2.2 RBAC 中间件
- **Task**task-004, task-005
- **输出**`middleware.ts` 鉴权 + 权限校验
- **验证**:越权 403权限 `alert:read` `rpa:operate` `pricing:markup:write`
- **状态**Done
- 执行记录 1service-token.ts 租户绑定 + rbac.ts requirePermission
- 执行记录 2service-token/rbac 单测 7 用例通过
### Step 2.3 限流
- **Task**task-098
- **输出**:令牌桶限流
- **验证**:超限返回 RATE_LIMITED
- **状态**Done
- 执行记录 1lib/gateway-retry.ts 5xx 单次重试 → 503 INTERNAL_ERROR
- 执行记录 2gateway-retry.test.ts 3 用例通过
---
## Phase 3 — 领域逻辑
> Task040~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
- 执行记录 1lib/math.ts roundHalfUp + engine applyMarkup/getMarkupPercent/applyMarkupToQuotes
- 执行记录 2math.test.ts + engine.test.ts 11 用例通过tsc 零错误
### Step 3.2 入参校验
- **Task**task-010, task-011
- **输出**`modules/quote/validation.ts`
- **验证**Mothership 官方限制;地址 place_id
- **状态**Done
- 执行记录 1validation.ts MOTHERSHIP_LIMITS + zod 校验 + unit-converter + cargo-hash
- 执行记录 2validation/unit-converter/cargo-hash 单测 TC-201~204 通过
### Step 3.3 幂等模块
- **Task**task-012
- **输出**`modules/quote/idempotency.ts`
- **验证**24h 同 request_id 返回同一 quote_id
- **状态**Done
- 执行记录 1idempotency.ts Redis L1 + DB 双保险 + P2002 冲突处理
- 执行记录 2idempotency.test.ts 5 用例通过
### Step 3.4 询价编排器
- **Task**task-013, task-014, task-019
- **输出**`modules/quote/orchestrator.ts`
- **验证**L1→L2→RPA 状态机完整
- **状态**Done
- 执行记录 1orchestrator submitQuote + quote-id 生成 + rpa-queue 占位入队
- 执行记录 2orchestrator.test.ts TC-104/105 + processing 路径 3 用例通过
### Step 3.5 Fallback 编排
- **Task**task-092
- **输出**`modules/quote/fallback-orchestrator.ts`
- **验证**RPA 失败 → L3 → STALE_FALLBACK 预警
- **状态**Done
- 执行记录 1handleRpaFailure L3 降级/失败分支 + alert_log 写入
- 执行记录 2fallback-orchestrator.test.ts TC-205/206 通过
### Step 3.6 置信度与四档结构
- **Task**task-095, task-109, task-111
- **输出**confidence_score + 4 档 quotes_json 结构
- **验证**TC-102 四档验收
- **状态**Done
- 执行记录 1confidence.ts + quote-completeness assertFourTiers + quote-id P2002 audit
- 执行记录 2confidence/quote-completeness/quote-id 单测 13 用例通过npm test 74/74
---
## Phase 4 — API 层(里程碑 M1
> Task015 → 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
- 执行记录 1route.ts 薄层:鉴权 + 限流 + orchestrator + ValidationError→400
- 执行记录 2quotes-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→done404 QUOTE_NOT_FOUND
- **状态**Done
- 执行记录 1quote-serializer.ts expired/confidence 格式化 + 越权 403
- 执行记录 2quote-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
- 执行记录 1history/route.ts 强制 page+size + customer_id 过滤
- 执行记录 2TC-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
- 执行记录 1GET list + PUT upsertRBAC pricing:markup:write + 0~30 校验
- 执行记录 2TC-502/505 单测通过isKnownCustomer 客户存在校验
### Step 4.5 询价超时处理
- **Task**task-018
- **输出**processing 30s 超时 → QUOTE_TIMEOUT
- **验证**TC 超时场景
- **状态**Done
- 执行记录 1timeout-sweeper.ts 10s 扫描L3→stale / 无L3→QUOTE_TIMEOUT
- 执行记录 2timeout-sweeper.test.ts 4 用例通过worker:scheduler 脚本已加
---
## Phase 5 — RPA
> Task020~029 → 110 → 093, 099, 100
### Step 5.1 BullMQ 队列
- **Task**task-020 ~ task-022
- **输出**`workers/rpa/queue.ts` + job 定义
- **验证**:入队/消费/重试策略
- **状态**Done
- 执行记录 1QuoteProvider 接口 + BullMQ quote-rpa 队列 + session-manager 登录态
- 执行记录 2rpa-queue 入队 + worker index 消费/熔断暂停103 单测通过
### Step 5.2 Playwright RPA Provider
- **Task**task-023 ~ task-028
- **输出**`workers/rpa/mothership-provider.ts`
- **验证**:地址联想点选 + 四档抓取
- **状态**Done
- 执行记录 1mothership.ts preCheck/fillAddress/fillCargo/scrapeQuotes/validateResult + error-mapper
- 执行记录 2validateResult + 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
- 执行记录 1job-handler 成功写 L2/L3/落库/偏差检测;失败走 fallback管理 API 三路由
- 执行记录 2GET /api/rpa/status + pause + manual-fallbackmiddleware 管理员路由已放行
---
## Phase 6 — 预警 / 审计
> Task050~057 → 094 → 105
### Step 6.1 预警服务
- **Task**task-050 ~ task-055
- **输出**`modules/alert/service.ts`
- **验证**4 种预警类型写入 alert_log
- **状态**Done
- 执行记录 1writeAlert 统一写入 + deviation/fallback/job-handler/sweeper 重构
- 执行记录 28 类预警(含 STRUCT_CHANGE/SESSION_EXPIRED/MANUAL_FALLBACK114 单测通过
### Step 6.2 预警 API
- **Task**task-056, task-057
- **输出**`app/api/alerts/` 列表 + resolve
- **验证**:管理员 RBAC分页
- **状态**Done
- 执行记录 1GET /api/alerts 分页+type/status 筛选POST resolve 更新 resolved
- 执行记录 2alerts-routes.test.ts 6 用例通过middleware 已放行 /api/alerts
### Step 6.3 审计日志
- **Task**task-094, task-105
- **输出**`modules/audit/` + AuditLog 写入
- **验证**:高危操作可追溯
- **状态**Done
- 执行记录 1writeAudit + recordSecurityEvent越权/注入/IP封禁写 SECURITY
- 执行记录 2audit/service.test.ts 3 用例quote-id/manual-fallback 已迁移
---
## Phase 7 — 观测指标
> Task101 → 102
### Step 7.1 三率指标
- **Task**task-101, task-102
- **输出**API/RPA/realtime rate 采集
- **验证**GD-4 指标可查询
- **状态**Done
- 执行记录 1modules/metrics/collector.ts Redis 计数器 + orchestrator/RPA/fallback/sweeper 埋点GET /api/metrics/dashboard
- 执行记录 2collector 7 用例 + dashboard 2 用例通过npm test 123/123middleware 已放行 /api/metrics
---
## Phase 8 — 前端
> Task060~069 → 106 → 063, 064, 079, 103, 104, 080
### Step 8.1 宿主内嵌查价组件
- **Task**task-106替代 075~077
- **输出**内嵌组件地址联想、托盘数、四档展示、3min 倒计时)
- **验证**异步轮询L2 命中无 RPA
- **状态**Done
- 执行记录 1Tailwind+Geist+UI 组件库QuoteForm/QuoteCard/CountdownTimer/QuoteResultPaneluse-quote-polling 退避轮询EmbeddedQuoteWidget + /embed-demo
- 执行记录 2pollQuoteUntilDone 3 用例通过npm test 126/126next 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/dashboardAdminLayout+JWT 本地存储403/not-found 错误页
- 执行记录 2预警列表分页+标记已处理+RPA 暂停入口;指标看板 GD-4 四卡;仅 admin 入口无 customer 卡片
---
## Phase 9 — 测试 / CI
> Task081~118 → 091
### Step 9.1 单元测试
- **Task**task-081 ~ task-091
- **输出**Vitest 覆盖核心模块
- **验证**:覆盖率达标
- **状态**Done
- 执行记录 1task-081~087 单测已覆盖 engine/validation/cargo-hash/idempotency/deviation补 surcharge=0test:unit 脚本 104 用例
- 执行记录 2npm 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-dbload-tests 三脚本go-no-go-check.ts
- 执行记录 2test:integration 33 + E2E 7 全绿go-no-go --mock PASS
### Step 9.3 CI 流水线
- **Task**task-091
- **输出**`.github/workflows/` 或等效 CI
- **验证**lint + test + build 全绿
- **状态**Done
- 执行记录 1ci.ymllint+test:unit+test:integration+builde2e-nightly.ymlE2E+k6+go-no-go
- 执行记录 2npm test 145/145lint 零错误playwright 7/7build 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);移除有害 SwitchDaemonWSL 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
- 执行记录 1PRD §8.2 + 技术设计 §7.2 补充匿名查价;关闭 Mock修复 session-manager 先判登录页再要凭据
- 执行记录 2实测 `www.mothership.com/quote` 404默认 URL 改 `dashboard.mothership.com`;真实冒烟需配置 MOTHERSHIP 凭据
---
## Phase 10 — RPA v1.2PRD v0.6 / 技术设计 v1.2
> Task119 → 120 → 121 → 122 → 123~131 → 132 → 133~135
### Step 10.0 环境与 Docker RPA 持久化
- **Task**task-119, task-120**与 Phase 0 Step 0.30.4 同任务**
- **输出**`.env.example` RPA env`docker-compose.yml` rpa_state volume
- **验证**:缺 `MOTHERSHIP_QUOTE_URLS` fail-faststorageState 跨重启保留
- **状态**Done见 Phase 0 Step 0.30.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.6probe 3/3
- **验证**`bash scripts/probe-rpa-3x.sh` 连续 3 次 exit 0
- **状态**Done
- 执行记录 1QuotePageAdapter 多 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
- 执行记录 1Hono + 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/3e2e-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 jobsecrets 缺失跳过);`go-no-go --mock` PASSintegration 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 跨平台录制入口
- 执行记录 2SOP 补充 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-fastFASTEST 改可选
- **根因**`.env` 中 `RPA_SELECTOR_FASTEST_OPTION=` 空;`assertRpaSelectorsEnv` 仍要求 8 项全填
- **修复**`RPA_REQUIRED_SELECTOR_ENV_KEYS`7+PRICEdev 后台进程预加载 `.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 0worker-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` 等 helperbrowser context 未定义
- **修复**`browser-eval-scripts.ts` 纯字符串脚本;全部 evaluate/waitForFunction 改传 string
- **状态**Done
- 执行记录 17 处违规调用点已迁移;禁止 function 引用传入 evaluate
- 执行记录 2`browser-eval-scripts.test.ts` 无 __name 断言通过
### Step 10.26 cargo hydrationmounted ≠ 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 boundaryprobeCargoOnWidget 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 空串 `""` 无效;未进 registryevaluate 内 `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/waitCargoStateReadyactive 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 + scrollIntoViewresolveQuoteEntry 仅 stabilizeheaded 默认 `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` 触发联想
- 执行记录 2mock 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
- 执行记录 2kernel 单测通过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
- 执行记录 2RPA 相关单测 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
- 执行记录 1pipeline 重写为 submit→network capture→strict validatefail-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 仅 telemetryQuote 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 才展开 drawerinput 探针在 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 的 popoverWeight/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 仍强制 expandDOM 诊断证实填址后 widget 无 cargo input仅「货运详情」步骤文案
- **修复**`isCargoUiPresent()` 为 false 时 skip expand/fill英文 chip 流保留原逻辑
- **状态**Done
- 执行记录 1`scripts/debug-cargo-chip.ts` 证实 headless 中文 UI 无 pallet/weight 节点
- 执行记录 2probe 已通过 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` 导出 resolvercapture 用 `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/CXHR / 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/RESPpost-submit url 日志;修复 async handler 竞态
- 执行记录 2`.env.example` 增 visibility 开关;不改 kernel/contract/selector
### Step 10.39 Business Ready Gatesubmit 前闸门)
- **根因**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` 认摘要文案+inputValuebusiness-ready 同步托盘s 模式
### Step 10.42 Cargo Commit Barrier
- **根因**summary chip 可见但 placeholder 仍可编辑 → 假锁定 → submit 无 quote flow
- **输出**`waitForCargoCommittedState`/`assertCargoCommittedState`;错误码 `CARGO_NOT_COMMITTED`
- **状态**Done
- 执行记录 1committed 需 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 轮询 commitwidget-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` 双地址 assertsubmit 未跳转 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.envworker 前摇探针 + 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/BlvdLoslistbox 双行未转 `主|副``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前端 AbortRedis 断连时轮询读不到 worker 结果
- **修复**:主 query 12s + 最多 2 个短 query 5s≥2 项即停;超时链提至 210sRedis 探针与不可用快失败
- **状态**Done
- 执行记录 1`address-suggestions.ts` 智能枚举;`constants`/`route` 210s/300s
- 执行记录 2`candidates-queue.ts` ensureRedisReady + redis_unavailable 区分
### Step 11.10 多候选枚举与报价超时对齐
- **根因**:联想首条即返回漏采其余 pac-item前端/后端报价超时 60sRPA 候选+询价合计超 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 后仅等 500msWindows 未及时释放 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 等待;候选联想 12sbounded 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.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
- 执行记录 1CLI 实网 pickup 4 / delivery 2 约 112sverify-address-candidates 142s 成功spawn 回退)
- 执行记录 2`candidates-queue.test.ts` + `worker-health.test.ts` 4/4 通过
---
### Step 11.16 街道-only 联想 label 解析失败
- **根因**`listSideCandidates` L91-96MotherShip 返回 `1234 Warehouse Street` 等仅街道行,`labelToCandidate` L377 返回 null
- **修复**`parseStreetOnlySuggestion` 补全 city/statepipe 行 `isMotherShipEnumerationRow` 不再误杀 streetToken
- **状态**Done
- 执行记录 1`address-suggestions.ts` 新增 street-only 解析pipe 副行 echo 判定
- 执行记录 2CLI 实网 pickup 3 项 / delivery 2 项;单测 16/16
### Step 11.15 地址枚举 ArrowDown 与 echo 过滤修复
- **根因**:枚举路径 `ArrowDown` 收起 listbox 仅留 1 项;`isMotherShipEchoCandidate` 仅比 display_label 把不同街道/城市候选误杀
- **修复**`forEnumeration` 跳过 ArrowDownlistbox 可见等待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
- 执行记录 1headed 双检查点录证地址锁定→存储快照→提交报价→HAR+Schema
- 执行记录 4A3 误报修复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.bestValuedecode 映射 bestValue/daysaxel URL 优先
- **验证**`quote-capture.test.ts` axel fixture 四档解析通过
- **状态**Done
- 执行记录 1`quote-contract-guard` lowest/bestValue + baseQuotePrice/finalPrice 价格键
- 执行记录 2`payload-decode` guaranteed.bestValue→lowestaxel 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询价 takepreheat/release API + 前端弹窗联动
- **验证**parked-quote-session 单测;驻留不可用回退 storageState 开页
- **状态**Done
- 执行记录 1`address-candidates` park 后不关页;`mothership` 优先 `takeParkedQuoteSession`
- 执行记录 2preheat/release BullMQ jobembed 弹窗打开预热、取消释放
### 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超时 300sjobId 改 `_`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 双开 Playwrightorphan 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` 同为 360ssweeper 在 RPA 完成前 `moveToFailed` active job
- **修复**`QUOTE_TIMEOUT_MS`=420sRPA 360s + 60s gracelock +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/42RPA 相关单测 30/30
- **状态**Done
- 执行记录 1耗时瓶颈在 page.goto/widget hydration 与 axel/quote 网络响应,非本地 fill
- 执行记录 2address-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=2296commit 闸门只验托盘数不验单托重量
- **修复**`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.16fill 无 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 超时(旧 workerQTE_0026 货物重量闸门拦截 457 vs 2079Lbs
### Step 11.39 AxelSelectionBridgesearch → 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 200commit 以网络证据为准
- **状态**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 实网验证)
- 执行记录 1debug-efb842 H2 CONFIRMEDH1/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` 封装 DOMcommit 状态机 + 分类诊断日志
- **状态**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 未加载 mockmock 仍调 ensureAddressSuggestionssubmit 仍校验双地址锁定
- **修复**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 放行
- 执行记录 2Worker 重启后 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 pacArrowDown 误触 Use my location键盘路径触发错误 placeId
- **修复**`mothership-styled-suggestion-click.ts` 全页扫描+浏览器内 clickadapter 优先 styled 点击;禁用无 pac 时 keyboard-bound
- **状态**Done
- 执行记录 1新增 CLICK_MOTHERSHIP_STYLED_SUGGESTION、cookie dismiss、probe/demo-seattle-miami 脚本
- 执行记录 2browser-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 交替 refillorchestrator 仅 widget submitdemo 五档断言 + session
- **状态**In progress
- 执行记录 1isPickupReady 去掉 dropdown 误判forceDismiss + chip 态空派送框fillAddress 后 releasePickup
- 执行记录 2enumerationMode 与 quote 路径分离prefetch 后 Escape/Tab 锁定提货 chip
### Step 11.47 delivery 门牌绑定 + 枚举/query 对齐人工页
- **根因**枚举首条即停resolveSelection 用无门牌 display_labeldirect quote 缺门牌 street
- **修复**:多 query 直到含门牌bindingLabels 优先完整街道axel 候选过滤门牌;候选排序
- **状态**Done
- 执行记录 1`enumerateAddressCandidates`/`ensureAddressSuggestions`/`enrichCandidateFromQuery`/`resolveSelection` 门牌优先
- 执行记录 2`demo-screenshot-case-a` QTE_0051 delivery 已含 5678lowest/bestValue 仍待 widget 双地址 commit + submit 路径
### Step 11.46 placeId 归一化 + axel-place-prefetch + direct axel/quote
- **根因**:复合 placeId 贪婪匹配DOM 点选不触发 GET placewidget 双地址互斥
- **修复**27 字符 ChIJ 截断prefetch GET placedirect POST axel/quotedemo 实测两组不同价
- **状态**Done
- 执行记录 1`normalizeAxelPlaceId`/`axel-place-prefetch.ts`/`axel-quote-direct.ts`orchestrator 双地址缓存走 direct
- 执行记录 2demo QTE_0048/0049 source=rpaWorker 日志 `direct axel/quote` 无 mock-chain-fixture
### Step 11.44 mock 全链路 PASS已废弃 fixture 方案,见 11.45
- **根因**MySQL 3307 宕机致 quote-create 500worker-rpa.log UTF-16 致 prove 日志正则失败L2 缓存撞 hash
- **修复**`dev:infra:start` 拉起 DBprove 读日志 UTF-16 解码 + MySQL 探活
- **状态**Superseded by 11.45
- 执行记录 1QTE_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_labelGET 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/fixedL2/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 lb9999×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 返回 undefinedplace 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
- 执行记录 1P0 包装 maps.event.addListener/trigger + instance.addListenerP2 fetch/XHR 劫持 axel/location/place
- 执行记录 2`place-diagnostic.test.ts` 通过;`npm run probe:place-diagnostic` 可 headed 实网取证
- **根因**:旧方案仅 recon 脚本注入 Autocomplete hookPlaywright/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-commitrecon-helpers 捕获 addListeneraxel-place-prefetch 优先 CDP pac
- 执行记录 2place-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
- 执行记录 2probe 仍卡在 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
- 执行记录 1RPA pickup/delivery 均 `commitState=COMMITTED`search→place 链 2/2 完整
- 执行记录 2延迟差 ~910s非阻塞未跑 quote差分脚本仅填址下一步 `prove:rpa-chain` 同址全链
### Step 11.68 差分脚本观测体验修复
- **现象**:首址前长时间无反应;填派送时界面跳动;填址成功后浏览器闪退
- **根因**`waitForQuoterWidgetHydrated` 无进度日志;`waitForDeliveryStreetInput` 循环点分区+Escape/Tab脚本 `finally` 强制 `closeBrowser`
- **状态**Done
- 执行记录 1`address-side` 派送框快路径;`compare-human-rpa-network` 尊重 `shouldKeepBrowserOpen`+阶段日志
- 执行记录 2headed 默认 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 detailsdelivery adapter 失败仍可走货物步
- 执行记录 2`address-commit.test.ts` 截图态用例;`settleAddressFieldsForCargo` 视觉短路
- **现象**:从上到下扫 DOMFreight 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 URLsource=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
- 执行记录 2pickup/delivery 均取消 `place`/`legacy` 长等待;联想点击后直接继续
### Step 11.75 Direct 优先 / Widget 兜底报价策略
- **需求**Direct 为主路径;仅 Direct 明确失败后才允许 Widget禁止并行/预热/默认 Widget
- **修复**`quote-strategy.ts` + `orchestrator`/`mothership` 串联;`pipeline` 闸门 `widgetFallbackAfterDirectFailure`
- **状态**Done
- 执行记录 1node `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
- 执行记录 2H01 系统探针 PASS4 档 rpa全量 10 组后台跑 `.rpa/human-doc-system-probe/`
### Step 11.77 系统探针失败组地址误匹配修复
- **现象**H06/H07 Bridge 误点H08 city-onlyH09/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_mismatchBridge 误点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 单测 1217s 通过
### 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 补档通过