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.

3171 lines
190 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.20 MotherShip 官网 Widget 档位对齐 + 专属卡车
- **根因**:仅解析 standard/guaranteedStandard 默认展示 API absolute lowest 与官网「最低价格」(bestValue) 不一致;`rates.dedicated` 未接入
- **修复**`lib/constants/mothership-ui-tiers.ts` 可扩展 Tab 注册表;`payload-decode` 动态解析 rates 顶层键;`prepareMotherShipStorageQuotes` 统一入库/API/前端过滤
- **状态**Done
- 执行记录 1`dedicated` 档解析 + `quote-card` 三 TabStandard 并存 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
- 执行记录 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 补档通过
### 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.90 Direct 失败误报 STRUCT_CHANGE 与预警文案修复
- **现象**LA→Pomona 询价超时;预警中心显示「页面结构变更」
- **根因**Direct 地址/入口类错误仍 Widget 兜底抛 STRUCT_CHANGE`resolveAlertRootCause` 未读 `detail.message`
- **修复**`NO_RETRY_CODES` 阻断 Widget 兜底;预警读 `message`;入口失败改 `RPA_FAILED`bootstrap 90s 超时
- **状态**Done
- 执行记录 1`mothership.ts` directFatal 跳过 widget`alert-presentation.ts` 读 message
- 执行记录 2`job-handler` QUOTE_ENTRY_UNAVAILABLE→RPA_FAILED`ensure-storage-state` 超时;单测补充
### Step 11.100 widget 双地址校验 California/Where to? 误判
- **现象**`assertPickupCommitted` 报提货地址从 widget 消失preview 含 `Los Angeles, CA Where to?`
- **根因**`widgetContainsConfirmedAddress` 要求全称州名;`isPickupReady` 只认 `Deliver to`axel 400 无报价仍 Widget 兜底
- **修复**:州名↔缩写匹配;派送标签含 `Where to?`axel 无报价→`CARRIER_NO_CAPACITY` 阻断 widget
- **状态**Done
- 执行记录 1`address-commit.ts` + `address-side.ts` + `selector-specs` + dual-patch-init
- 执行记录 2`quote-from-request.ts` 400 映射;单测 LA/CA + Where to?
### Step 11.101 Widget 全链 LA→Pomona 探针与首页内嵌报价
- **需求**MotherShip 官网 Widget 可出 $81.71;验证 `runMothershipQuoteKernel` Widget fallback
- **探针**`npm run probe:widget-la-pomona`;填址/货物/submit 已通过axel/quote contract=0
- **根因**headless 下 PAC `combobox-dead`preknown-synthetic 未锁定顶栏双街道 chipsubmit 不发报价 API
- **已修**chip 态 City/ST 校验、settle vault、Where to?、首页内嵌报价检测、submit 软等待、skipAddressLock
- **状态**In Progress待 PAC 点选/legacy-click 或 DOM 价抓取)
- 执行记录 1`probe-widget-la-pomona-quote.ts` + address-commit/address-side/post-submit/quote-strategy
- 执行记录 2生产路径走到 submit未捕获 axel/quote与人工截图 $81.71 差距在地址 chip
### Step 11.84 Priority1 FTL 人机协作录制
- **需求**FTL 整车自动填至卡住,人工补全并记录操作直至报价
- **输出**`human-assist-recorder.ts`、`runPriority1HumanAssistChain`、`npm run visual:priority1-human-assist`
- **状态**Done
- 执行记录 1Step 2 软自动(失败 warn 不 throw+ DOM 事件录制 + 报价轮询保存 `.rpa/recordings/priority1-human-*.json/js`
- 执行记录 2人机模式强制 headedCLI 默认 F01-ftl-dry-van-full
### Step 11.85 Priority1 全矩阵 benchmark 失败项修复
- **根因**Freight Class 观测读到错误 selectOpen 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` 跳过自动勾选
- 执行记录 22026-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 人工录制;批量 F04F07 卡在 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 无即时报价 → 人工跟进终态
- **需求**F08F11 等提交后出现 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
- 执行记录 124 条全部 `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 ≤10000Liftgate 仅 Large Straight
- 执行记录 2benchmark/export 前置 `validatePriority1FormConstraints` 校验
### Step 11.95 Priority1 系统查询链路接入生产
- **需求**24 条 canonical 跑通后,人工可在系统内提交 Priority1 询价并拿到即时报价或人工跟进终态
- **输出**`run-quote.ts` 接入 `visual-priority1-full-chain`job-handler `manual_followup`serializer + UI
- **状态**Done
- 执行记录 1worker `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
- 执行记录 2API/前端将英文框架异常转为中文提示,保留业务校验原文
### Step 11.99 Priority1 询价处理失败修复
- **需求**POST /api/priority1/quotes 返回「Priority1 询价处理失败」
- **输出**orchestrator 入队失败可读错误markQuoteStarted 改 safeRecordquote_id 冲突重试
- **状态**Done
- 执行记录 1移除提交前 await markQuoteStartedRedis 异常不再阻断入队前流程)
- 执行记录 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 摘要优先;按卡片顺序对齐 rankenrich 回填承运商
- **状态**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报价轮询仅在有金额时结束
- 执行记录 2worker `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
- 执行记录 1worker `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_` Keyenv `HOST_SERVICE_TOKENS` 继续兼容
- 执行记录 2middleware 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 转换
- 执行记录 2MotherShip 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 次 HTTPpickup/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-4TTL 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 quoteAPI 快乐路径)
- **输入**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 客户 Key 加密保存并可回显
- **输入**:客户管理需把当前有效 API Key 保存在客户资料中,方便管理员随时查看
- **验证**:新建/轮换后列表可直接看到当前有效 Key历史未存原文的旧 Key 提示需轮换
- **状态**Done
- 执行记录 1`service_api_key.token_ciphertext` 迁移;用 `JWT_SECRET` 派生 AES-GCM 密钥加密保存原文
- 执行记录 2客户列表新增当前 Key 回显;`token-crypto.test.ts` 覆盖加解密往返
### Step 12.16 Widget 地址联想提速preknown 快速路径
- **输入**preknown 路径(已从 MotherShip 选择过每次仍走联想等待512s+ 样式行等待2.510s整体超 30s地址联想点击失败no-dom-row
- **验证**preknown 直接 synthetic commit跳过联想等待`ADDRESS_SUGGESTION_TIMEOUT_MS` 3s、`ADDRESS_CANDIDATE_SUGGESTION_TIMEOUT_MS` 6s`ADDRESS_SIDE_COMMIT_TIMEOUT_MS` 6s
- **状态**:已回滚(用户要求恢复原始联想链路)
- 执行记录 1`mothership-address-adapter.ts` preknown 分支前置:先调 `triggerAddressInputOnly` 填字,立即 `trySyntheticPreknownCommit`,不等联想行/bridge 搜索结果
- 执行记录 2已用 `git checkout HEAD -- workers/rpa/address-adapter/mothership-address-adapter.ts workers/rpa/address-commit.ts lib/constants/rpa.ts` 回滚
### Step 12.17 Widget 联想地址「随便选一个」视觉确认降级
- **输入**styled-click 返回 `no-dom-row`keyboard + prefetch 也失败;用户要求:联想出来就点第一个,不要等特定 placeId API 回调,用视觉确认
- **验证**keyboard 失败后走 `ArrowDown→Enter`,等 600ms`isAddressPresentInWidget` 视觉确认;`allowVisualBypass=true` 对所有地址均开放视觉放行
- **状态**Done
- 执行记录 1`mothership-address-adapter.ts` 在 keyboard miss 后插入 `selectFirstSuggestionVisual`ArrowDown+Enter→视觉确认成功则直接 succeed
- 执行记录 2`allowVisualBypass` 由 `!selectedFromMothership` 改为 `true`,避免 preknown 地址点击成功后因无 placeId 回调而被拒绝
### Step 12.18 Widget 联想鼠标坐标点击 + 截图视觉闸门
- **输入**:用户要求仅以网站可见状态(截图等价)判定联想是否选中;`getByText` 点击日志不可信preknown 跳过 legacy 点击导致 widget 无地址
- **验证**`mouseClickSuggestionUntilVisualConfirmed` 用 `page.mouse.click` 点联想行坐标,每次点击后截图落盘并以 `isAddressPresentInWidget` 判定;成功才 succeed
- **状态**Done
- 执行记录 1新增 `address-suggestion-mouse.ts` + `COLLECT_SUGGESTION_MOUSE_TARGETS`adapter 主路径优先鼠标视觉点击;移除填址中途 `scrollQuoteWidgetIntoView({force:true})`
- 执行记录 2放宽 `COLLECT_WIDGET_SUGGESTION_LABELS` 宽高阈值width>180 height<=100`lockPickupIntoChip` 改鼠标视觉确认后跳过键盘
### Step 12.19 联想定位对比诊断(人工录制 vs 自动化)
- **输入**:用户要求运行自动化并对比人工录制,找出联想定位失败根因
- **验证**`npm run probe:suggestion-compare` 输出 report.json + 截图;`committedVisually` 在填字后=false、getByText 点击后=true
- **状态**Done
- 执行记录 1新增 `scripts/probe-suggestion-dom-compare.ts`根因listbox/PAC=0、输入框文字误判已确认、getByText partial 可见
- 执行记录 2`isAddressCommittedVisually` 排除输入框完整查询串;`clickSuggestionLikeRecording` 对齐 codegen `getByText('StreetLos')`
### Step 12.20 chip 态提货锁定闸门放宽
- **输入**:提货 recording-click 成功但 `settlePickupBeforeDelivery` 报未锁定widget 已有 chip + Where to?
- **验证**`isPickupReadyForDeliveryInput` chip 态inputs=[])且 Deliver to/Where to? 可见即放行
- **状态**Done
- 执行记录 1`address-commit.ts` chip 分支增加 deliverPrompt 视觉判定
- 执行记录 2`probe:widget-la-pomona` exit 0standard/bestValue=$81.71,约 194s
### Step 12.21 20s 快速报价路径RPA_FAST_QUOTE
- **输入**:探针约 215s提货 `ensureAddressSuggestions` 循环 8 个 query 各等 515s
- **验证**`npm run probe:widget-la-pomona:fast` durationMs < 20000 且档位 > 0
- **状态**Done
- 执行记录 1`lib/rpa/env.ts` 新增 `isRpaFastQuoteMode`/`resolveAxelSearchWaitMs`pickup `maxQueries:1`;跳过 Direct
- 执行记录 2`probe:widget-la-pomona:fast` 约 97s原 215spreknown rush 预热+prefetch<20s 需热浏览器+recording 点选
### Step 12.22 chip 态提货误判「联想未确认」
- **输入**:截图 chip「Los Angeles, CA」+ Deliver to 已现,脚本仍报联想未点中
- **验证**`widgetContainsConfirmedAddress` chip 折叠 + `isAddressCommittedVisually` 识别 Deliver to
- **状态**Done
- 执行记录 1州名 California↔CA 映射;门牌折叠态以 city+state 放行
- 执行记录 2`isPickupChipCommitted` 优先闸门;隐藏 input 残留查询串不再误判
### Step 12.23 唯一视觉 chip 确认(两行门牌 + City, ST
- **输入**:仅打字未点联想却被脚本误判;用户要求唯一标准=截图 chip 视觉
- **验证**`evaluateMotherShipAddressChipLines` + `DETECT_MOTHERSHIP_ADDRESS_CHIP`;打字态 active-search-typing=false
- **状态**Done
- 执行记录 1新增 `address-chip-visual.ts``isAddressCommittedVisually` 仅委托 chip 探测
- 执行记录 2移除 place200/chip折叠放行prefetch 无 chip 视觉则继续点选 fallback
### Step 12.25 报价前闪退:内联货物 Escape 误重置 + 探针强关浏览器
- **输入**:填址/货物完成后浏览器闪退,未点 Get a freight quote
- **验证**`dismissCargoPopoverOnly` 仅 overlay 弹层 Escapeblind/rush 跳过表单就绪闸门仍点报价headed 探针失败保留窗口
- **状态**Done
- 执行记录 1根因 `isCargoFieldsVisible` 内联字段触发 3 次 Escape 重置地址 + `isWidgetQuoteFormReady` 拦截 + probe catch 调 `closeBrowser`
- 执行记录 2`isCargoOverlayPopoverOpen``fillLandingFreightPopover` 不再 Escapeblind 仍点报价;`shouldKeepBrowserOpen` 保窗
### Step 12.26 Widget 分 Tab 报价补档Shared / Dedicated truck
- **输入**:探针仅 `standard/bestValue=$81.71`,官网另有 Dedicated truck=$246.11
- **验证**`supplementWidgetTruckTabQuotes` 切 Dedicated Tab 补 axel/DOM探针输出两档真实价
- **状态**Done
- 执行记录 1首包 axel 常仅 standard/bestValueDedicated 需 Tab 切换后补抓
- 执行记录 2新增 `widget-truck-tab-quotes.ts`pipeline 合并;探针 `formatWidgetQuoteTierLabel`
### Step 12.27 Dedicated Tab 点击失败修复
- **输入**Step 12.26 后仍仅 1 档;日志无 DOM/axel 补档
- **验证**evaluate+shadow DOM 点 Tab点击前挂 axel listener合并全部 axel body
- **状态**Done
- 执行记录 1根因 `getByRole/getByText exact` 点不到 sign-up pill Tab
- 执行记录 2iframe/shadow 深搜 + DOM 读价pipeline 传入 axelBodies
### Step 12.28 强制点击 Shared/Dedicated Tab + 截图读价
- **输入**Tab 仍未点上,仅 1 档
- **验证**:始终依次点 Shared truck、Dedicated truckPlaywright+evaluate 双重点击;读醒目 $ 价;失败截图落 `.rpa/probe-widget-la-pomona/`
- **状态**Done
- 执行记录 1去掉 hasServiceLevel 跳过Tab 切换价为展示权威
- 执行记录 2`readDominantQuotePrice` 按可见面积取卡片价;每 Tab 截图 ok/fail
### Step 12.29 Tab 读价覆盖 axel 导致时效校验失败
- **输入**:两档价已读到,探针报 `standard/bestValue 时效为空` 后关窗
- **验证**`mergeWidgetTabQuotePrice` Tab 只覆盖金额,保留 axel 时效;无 axel 时用「待确认」
- **状态**Done
- 执行记录 1根因 Tab 条目整表覆盖 mergedtransitDays=— 触发 validateQuoteSchema
- 执行记录 2`mergeWidgetTabQuotePrice` + 单测 validateQuoteSchema 通过
### Step 12.30 embed-demo 直连无运力仍走 Widget 回退
- **输入**embed-demo LA→Pomona 显示「该线路暂无可用报价」;探针 Widget 有 $81.71/$246.11
- **验证**axel 直连 CARRIER_NO_CAPACITY 不阻断 `runMothershipQuoteKernel`Worker 浏览器 Tab 补档
- **状态**Done
- 执行记录 1根因 `NO_RETRY_CODES` 含 CARRIER_NO_CAPACITY 致 `mothership.getQuote` 未启浏览器
- 执行记录 2`lib/rpa/direct-quote-fallback.ts``isDirectErrorFatalWithoutWidget` 放行 Widget
### Step 12.31 embed-demo 已确认候选自动 blind 填址
- **输入**LA Warehouse→Pomona 7th 在 embed-demo 超时Worker 卡在 pickup `selection_failed` / 联想行点不中
- **验证**:两侧 `selectedFromMothership`+有效 optionId 时自动 `blindAddressFlow`;跳过 chip/commit 闸门submit 同步 blind
- **状态**Done
- 执行记录 1根因生产 Worker 未设 `RPA_BLIND_ADDRESS_FLOW`,严格 mouse/chip 与探针路径不一致
- 执行记录 2`resolveBlindAddressFlow`/`isBlindAddressFlowActive``StateEngine.blindAddressFlow`address/cargo/submit 贯通
### Step 12.32 Pomona 方位缩写 W 7th St 候选误排与 RPA 探针
- **输入**`600 W 7th St` 无法报价,`600 East 7th Street` 可报价MotherShip 将二者视为不同坐标
- **验证**:缩写方位二次 search 补全 West 7th Street候选排序惩罚相反 EastRPA 探针优先 display_label + 展开街道
- **状态**Done
- 执行记录 1根因 `600 W 7th St` 候选列表首位为缩写坐标,且与 East 7th 同门牌混淆
- 执行记录 2`lib/address/street-normalize.ts``candidates.ts` 双 query`buildRecordingStyleTextProbes` 增强
### Step 12.33 货物单位切换应换算数值并刷新报价
- **输入**500kg 与 500lb、cm 与 in 切换后报价相同;用户期望单位变化保持物理量并影响询价
- **验证**:界面保留用户输入单位与数值;后端 kg/cm 换算为 lb/in 并小数进一;改货物后清除旧报价
- **状态**Done
- 执行记录 1根因 UI 自动改数字与后端未统一向上取整;改单位后仍显示旧报价
- 执行记录 2`validation.ts` toAxelWhole*;表单仅切换单位不改数;询价提示显示换算后 lb/in
### Step 12.34 管理员万能登录密码
- **输入**:管理员需一个密码可登录管理端与任意客户 embed-demoCUST_001/002 等)
- **验证**`/login` 用有效用户名 + `MASTER_LOGIN_PASSWORD` 可登录;`/embed-demo` 用有效客户编号 + 万能密码可登录
- **状态**Done
- 执行记录 1新增 `modules/auth/master-password.ts``auth/login` 与 `embed-demo/login` 接入 `isMasterLoginPassword`
- 执行记录 2`.env` 示例 `MASTER_LOGIN_PASSWORD=mmgy2026master`;未配置时功能禁用;单测 `master-password.test.ts`
### Step 12.35 Flock Freight 需求与技术设计模块(文档先行)
- **输入**:新增报价源 Flock Freight`https://app.flockfreight.com/get-a-quote`);托盘 420、总重≤45000lb、尺寸 L/W/H≤636/102/108弱注册两档结果
- **验证**PRD §14 + GD-16技术设计 §7.6domain-rules 引用 v0.7/v1.3
- **状态**Done
- 执行记录 1`docs/查价系统-PRD.md` 升 v0.7,新增第 14 章与附录 E范围矩阵/扩展点同步
- 执行记录 2`docs/查价系统-技术设计.md` 升 v1.3§7.6 Provider/硬限/env/队列;目录与 `.env.example` 示意补齐
### Step 12.36 Flock Freight 落地实现(校验/API/UI/RPA
- **输入**PRD §14 + 技术设计 §7.6中文界面kg/cm 自动换算进一500kg→1103lb
- **验证**`vitest` flock 单测embed 切换 Flock 表单Mock 下 `probe:flock-rpa` exit 0
- **状态**Done
- 执行记录 1`FLOCK_LIMITS`+validation+orchestrator+`POST /api/flock/quotes`+queue job `flock-quote`+中文表单/两档结果卡
- 执行记录 2单位切换复用 cargo-form-unitsembed 第三 Tab单测覆盖硬限与 500kg→1103lb
### Step 12.37 Flock 人工录制脚本codegen + 人机协作)
- **输入**:链路未打通,需人工录制定位按钮、熟悉官网流程
- **验证**`npm run record:flock` / `visual:flock-human-assist`;产出 `.rpa/recordings/flock-*.js`
- **状态**Done
- 执行记录 1新增 `record-flock.ts/.sh/.ps1`、`workers/rpa/flock/demo-input.ts`、canonical 路径与中文清单
- 执行记录 2`visual:flock-human-assist` 分步 pause+截图;`record:flock` 写入 package.json
### Step 12.37 Flock 表单 UI 回归修复(单位/尺寸提示/取货日)
- **输入**`flock-quote-form` 被回滚,英制/公制切换按钮复现;尺寸硬限提示与取货日 min 丢失
- **验证**:无公英制切换按钮;重量/尺寸独立下拉切换不改数636/102/108 提示;取货日不早于今天
- **状态**Done
- 执行记录 1根因表单文件回滚至 `switchUnit`+`displayUnitToCargoUnits` 旧实现
- 执行记录 2恢复 MotherShip 式单位下拉+硬限提示+`pickup-date` 校验;移除 `unitImperial`/`unitMetric`
### Step 12.38 Flock RPA 录制链路打通selector + visual-chain
- **输入**`.rpa/recordings/flock-manual-20260713-152051.js` 人工录制成功;对齐 Priority1 visual-chain 模式
- **验证**`vitest` quote-extract`RPA_MOCK_MODE=true npm run probe:flock-rpa`;真实模式 worker 消费 `flock-quote`
- **状态**Done
- 执行记录 1新增 `flock-selector-specs`/`form-interactions`/`quote-extract`/`visual-chain`submit 用录制 testId
- 执行记录 2`run-quote` 走 visual-chain+storageState注册页 combobox 对齐录制;`.env.example` FLOCK_SELECTOR 注释
### Step 12.39 Flock 无痕查价(每轮新邮箱/无 storage
- **输入**:用户要求每次查价不加载 storage清空 cookie使用新的免费公开邮箱
- **验证**`vitest` fresh-account/env`FLOCK_FRESH_SESSION_PER_QUOTE=true npm run probe:flock-rpa`
- **状态**Done
- 执行记录 1新增 `FLOCK_FRESH_SESSION_PER_QUOTE`、`lib/flock/fresh-account.ts`(邮箱池/+别名、Chrome `--incognito`
- 执行记录 2`visual-chain` 无痕不写 storage、禁用 storage 回退;`batch-flock-stress` 默认无痕模式
### Step 12.40 Flock 货物随机在拖车线性英尺内
- **输入**:官网报 `Linear feet of shipment is larger than trailer`(如 18托×403in
- **验证**`vitest` flock-limits/batch-flock-stress随机货物 `fitsFlockTrailerCargo`
- **状态**Done
- 执行记录 1新增 `estimateFlockLinearFeet`/`fitsFlockTrailerCargo`≤53'validation 拦截
- 执行记录 2重写 CARGO_PRESETS + `randomCargo` 按托盘数倒算可允许长度;电话改用有效美区号
### Step 12.41 Flock 一档即成功 + 20s 快测
- **输入**:有任意一档报价即成功;提交后最多等 20s 截图,立即下一组
- **验证**`batch:flock-stress` 10 轮;清空 flock-stalls
- **状态**Done
- 执行记录 1`FLOCK_MIN_QUOTES=1`、`FLOCK_QUOTE_WAIT_MS=20000`wait 任意报价
- 执行记录 2注册填表加速batch 默认 10 轮/不重试/清空 stalls
### Step 12.42 Flock qqdna 邮箱池探针
- **输入**http://qq.qqdna.com/more.php 号码 + @qq.com每轮全新无痕会话
- **验证**`FLOCK_EMAIL_MODE=qqdna` 批量 3 轮;结果 0/3 account_rejected
- **状态**Done
- 执行记录 1新增 `lib/flock/qqdna-email-pool.ts``FLOCK_EMAIL_MODE=qqdna` 轮询取号
- 执行记录 23 轮全新会话均被拒We had an issue creating your account号码库 QQ≠可用 Work email
### Step 12.43 Flock 注册拒因转向(非邮箱)
- **输入**:真实 Outlook/Gmail 仍被拒;用户确认非邮箱问题
- **验证**:注册失败文案与姓名/公司生成更真人化
- **状态**Done
- 执行记录 1排除邮箱后优先怀疑自动化指纹、电话复用、IP 频控、Guest### 机器人字段
- 执行记录 2`createFreshFlockTestAccount` 改用真人英文名+物流公司名;错误提示同步更新
### Step 12.44 Flock 批量每邮箱 3 次 + 随机 QQ 10 组
- **输入**10 组随机 ZIP/货物;`FLOCK_EMAIL_MODE=random_qq`;每邮箱 3 次后切换
- **验证**`FLOCK_QUOTES_PER_EMAIL=3` + `batch:flock-stress` 10 轮
- **状态**Done
- 执行记录 1`getFlockQuotesPerEmail` + `runBatchOneBrowser` 满 3 次换新邮箱/浏览器
- 执行记录 210 组 random_qq 探针 **10/10 通过**4 个邮箱各 3/3/3/1 次,总耗时 237s
### Step 12.45 Flock 生产链路接入Worker 会话复用)
- **输入**:批量验证的 random_qq + 每邮箱 3 次策略接入 RPA Worker
- **验证**`vitest` worker-session/envWorker 消费 `flock-quote` 复用浏览器
- **状态**Done
- 执行记录 1新增 `worker-session.ts` + `isFlockWorkerReuseSession``run-quote` 走会话复用
- 执行记录 2`flock-job-handler` 对齐 `FLOCK_MIN_QUOTES``.env.example` 默认 random_qq/3 次
### Step 12.46 Flock 报价上限自动换邮箱
- **输入**`FLOCK_QUOTE_LIMIT / Thank you for your interest` 未触发新邮箱
- **验证**`shouldRotateFlockSession` 含 quote_limitworker-session 同 job 内最多换 3 邮箱重试
- **状态**Done
- 执行记录 1根因在 `worker-session.ts` 仅处理拒号,上限错误直接返回失败
- 执行记录 2入口/表单/提交三处检测上限并 `closeSession` + 新 random QQ 重试
### Step 12.47 Flock 可选自填注册信息
- **输入**:用户希望自填邮箱/电话等,而非全部系统随机
- **验证**:表单附加项 + API `registration` + RPA 合并账号
- **状态**Done
- 执行记录 1`FlockRegistrationOverride` + validation + `resolveFlockQuoteAccount`
- 执行记录 2`flock-quote-form` 可折叠附加项Worker/visual-chain 使用用户指定字段
### Step 12.48 Flock 填表字段间 1s + 换网后 10 组探针
- **输入**:换网;字段间 `FLOCK_FIELD_PAUSE_MS=1000`random_qq 每邮箱 3 次
- **验证**`batch:flock-stress` 10 轮
- **状态**Done
- 执行记录 1字段间 1s换网后 **3/10 OK**(同邮箱满 3 次后第 4 组 account_rejected随后浏览器被关 fatal
- 执行记录 2失败与网站 Worker 共 IP/上限页并发踩踏相关;`.env` 已对齐并重启 Worker
### Step 12.49 Flock 网站链路对齐 batch 配置
- **输入**embed-demo 慢 + 一点击导致本地探针失败
- **验证**Worker 日志 `emailMode=random_qq reuseSession=true quotesPerEmail=3`
- **状态**Done
- 执行记录 1Worker 日志确认 `emailMode=alias reuseSession=false`;上限页重试空耗 ~80s
- 执行记录 2`.env` 补齐 random_qq/会话复用/无痕;需重启 worker
### Step 12.50 Flock embed 防互踩(本机串行锁)
- **输入**embed 与 batch 共 IP 互踩导致失败/极慢
- **验证**exclusive-lock 单测;入队忙时中文拒绝;上限不重试
- **状态**Done
- 执行记录 1`.rpa/flock-rpa.lock` + Worker/batch 互斥;上限/拒号 `retryable:false`
- 执行记录 2embed-demo Flock 提示串行;探针占用时入队直接失败
### Step 12.51 Flock embed 快失败(缩短 34 分钟等待)
- **输入**embed 比 batch 慢 34 分钟才出结果/失败
- **验证**:上限不重试换号;表单等 20s前端轮询 90s
- **状态**Done
- 执行记录 1根因=单 job 3 次换号×45s 等表单+1s 字段停顿batch 每轮只试 1 次
- 执行记录 2`SESSION_MAX_ROTATIONS=1`、上限快失败、字段停 300ms、`FLOCK_POLL_TIMEOUT_MS=90s`
### Step 12.52 Flock embed 对齐 batch 有效路径
- **输入**:本地 batch 1/1 成功、embed-demo 连续失败(上限/拒号)
- **验证**`.env` 无重复键embed 走无痕全链+拒号轮换vitest
- **状态**Done
- 执行记录 1`WORKER_REUSE_SESSION=false` + `FIELD_PAUSE_MS=1000` + `SESSION_MAX_ROTATIONS=3`;删除快失败覆盖块
- 执行记录 2`runFlockVisualChainWithRotations` 每轮新浏览器换 QQworker-session 失败必 `closeSession`
### Step 12.53 Flock embed 前端轮询超时修复
- **现象**RPA 仍在跑页面报「Flock 询价超时,请稍后重试」
- **根因**`flock-quote-widget` 客户端读 `FLOCK_POLL_TIMEOUT_MS` 无效,回退 90s3 轮换需 >150s
- **状态**Done
- 执行记录 1根因在 `lib/flock/poll-timeout.ts` 缺 `NEXT_PUBLIC_`,默认仍 90000
- 执行记录 2默认 210s + `.env` 补 `NEXT_PUBLIC_FLOCK_POLL_TIMEOUT_MS=210000`
### Step 12.53 Flock embed 构建 node:crypto 修复
- **根因**`flock-quote-widget` 引用 `lib/flock/env.ts`,顶层 import `fresh-account.ts``node:crypto`)被打进客户端 bundle
- **验证**`npm run build` 通过;`/embed-demo` 可加载
- **状态**Done
- 执行记录 1新增 `lib/flock/poll-timeout.ts`(无 Node 依赖widget 改从此处读轮询超时
- 执行记录 2`env.ts` 改为 re-export `getFlockPollTimeoutMs`,服务端逻辑不变
### Step 12.54 Flock Network 抓包 POCHAR / quote-rate 接口)
- **输入**:评估是否可像 MotherShip axel/quote 一样直连内部 JSON绕过填表
- **验证**`npm run capture:flock-har` 产出 `.rpa/flock-network-poc/*.har` + summary`analyze:flock-har`vitest har-analyze
- **状态**Done
- 执行记录 1真实抓包命中 `POST api.flockfreight.com/user/quotes` 201含 `fulfillmentOptions[].rateUsd`15 档)
- 执行记录 2结论见 `.rpa/flock-network-poc/FINDINGS.md`;注册失败仍可出 JSON 价;下一步补抓 request body 再做 Direct POC
### Step 12.55 Flock Direct 询价user/quotes JSON
- **输入**POC 命中 `POST api.flockfreight.com/user/quotes`;需落地 Direct-first + DOM fallback
- **验证**`vitest` payload/map`FLOCK_QUOTE_MODE=direct npm run probe:flock-rpa` 出两档价;默认 `direct_then_dom`
- **状态**Done
- 执行记录 1`route` 补抓 request body`quote-payload`/`map-fulfillment-options`/`direct-quote``FLOCK_QUOTE_MODE`
- 执行记录 2`run-quote` Direct 优先;失败回退 visual-chain`.env.example` 注明模式
### Step 12.56 Flock Direct 十组样例验收
- **输入**:验证 Direct 多线路能否稳定出价
- **验证**`FLOCK_QUOTE_MODE=direct npm run batch:flock-direct-10` → **10/10 OK**
- **状态**Done
- 执行记录 1新增 `scripts/batch-flock-direct-10.ts`;跨区 ZIP×货物差异化 10 组
- 执行记录 2全成功部分线路仅 1 档(无 FlockDirect 或无 Standard属承运能力而非接口失败
### Step 12.57 Flock Direct 接入 embed-demo 链路
- **输入**embed-demo Flock Tab 需走 Direct旧 Worker 仍填表注册失败
- **验证**`.env` `FLOCK_QUOTE_MODE=direct_then_dom``smoke-flock-embed-direct` → done 两档Worker 日志 `quoteMode` + `flock-direct OK`
- **状态**Done
- 执行记录 1写入 `.env` + 重启 dev技术设计 §7.6 改为 Direct 优先worker 启动打印 quoteMode
- 执行记录 2`QTE_20260714_0006` Direct OK ref=RZ4-DJMQ $1970/$1183新增 smoke 脚本
### Step 12.58 去掉 Flock 串行锁提示(加价仍属管理端)
- **输入**:取消 exclusiveHint 文案;加价配置仅管理端 `/admin/markup`,演示端不配
- **验证**Flock Tab 无串行锁 WarningBannerembed-demo 无加价编辑 UI
- **状态**Done
- 执行记录 1移除 `FLOCK_UI.exclusiveHint` 与 Flock WarningBanner
- 执行记录 2撤回误加的 embed-demo 加价表单/API加价仍走管理端
### Step 12.59 Flock 直连 + 登录 RPA 双路线
- **输入**DOM 保障路径改为账号密码登录Direct 保持匿名 `user/quotes`
- **验证**vitest login/env`probe:flock-login`(需本地填凭据);无凭据不注册瞎填
- **状态**Done
- 执行记录 1`FLOCK_LOGIN_*` + `workers/rpa/flock/login.ts`;有凭据时关闭 fresh/random/换号
- 执行记录 2visual-chain/run-quote/worker-session 接入登录;技术设计 §7.6 DOM 补登录语义
### Step 12.60 客户级承运商账密MotherShip + Flock
- **输入**按客户持久化官网账密有账密强制登录查价Flock 跳过 Direct无账密走原线路
- **验证**vitest 加解密 + Flock 强制 DOMadmin GET/PUT provider-credentials
- **状态**Done
- 执行记录 1表 `customer_provider_credential` + `encryptSecret`;管理端「承运商账密」
- 执行记录 2Flock/MS Worker 运行时按 customerId 解密;凭据不进 Redis job
- 执行记录 3客户管理「新建客户」增加「绑定查价网站账密」勾选可同时填 MotherShip/Flock列表按钮改为「查价网站账密」双栏编辑
- 执行记录 4客户账密强制 path=dom-login解密失败不回落 Direct清 cookie 重登 + 禁共享 storage + Direct 护栏
- 执行记录 5登录后 Quick「Let's build a quote」表单适配Type/Packaging/Class/Description + Generate my quote
### Step 12.61 MotherShip 登录后查价界面录制
- **输入**:客户有账密时官网为登录态查价 UI须人工 headed 录制,与匿名 widget 隔离
- **输出**`record-mothership --logged-in``docs/rpa-mothership-logged-in-sop.md`;登录基线 fixture + `.env.example` 占位
- **验证**`vitest record-mothership.test.ts`;产物前缀 `mothership-logged-in-*` / storage 隔离GD-15 仅匿名强制
- **状态**Done
- 执行记录 1`--logged-in` 独立 recordings/storage允许 dashboardSOP + LOGIN/LOGGED_IN selector 占位;登录页实探回填附录 A
- 执行记录 2查价段待账密续录`.env`/DB 无凭据);`finalize-mothership-logged-in-recording.ts` 可摘抄最新 js
### Step 12.62 MotherShip 登录后一级查价 UI创建新货件
- **输入**:录制截图 + `mothership-logged-in-20260715-105824.js`;有账密客户看到登录后一级表单
- **输出**`components/mothership/mothership-logged-in-shipment-form.tsx`embed `providers.has_password` 切换;`?ms_logged_in=1` 预览
- **验证**vitest 常量embed-demo 登录后 MotherShip Tab 显示「创建新货件」
- **状态**Done
- 执行记录 1还原地址/附加服务/可提货时间/多行货物login+me 返回 providers有账密或预览参数切 UI
- 执行记录 2「继续」暂存一级草稿报价 API 后续对接);录制脚本含明文账密勿入库/勿提交
### Step 12.63 MotherShip 登录后二级 Details + 报价栏
- **输入**:截图 Quotes>Details一级「继续」后进入详情并显示承运商报价
- **输出**`mothership-logged-in-details-form.tsx` + `mothership-logged-in-quote-sidebar.tsx`embed 步骤切换
- **验证**vitest 预填/预览价embed `?ms_logged_in=1` 填一级点继续见详情+右侧报价
- **状态**Done
- 执行记录 1二级预填地址/附加服务/货物/就绪时间FBA 预约块;货物扩展字段与受管制声明
- 执行记录 2右侧 XPO/SAIA/ABF 预览价(界面还原,标注非实时 RPA可返回一级
### Step 12.64 MotherShip 登录后地址联想Axel suggest
- **输入**:一级自由文本需 MotherShip 联想点选;同源 Axel location/search
- **输出**`POST /api/addresses/mothership-suggest``MothershipAddressSuggestField`;继续闸门强制 confirmed
- **验证**schema/映射/Details 预填单测embed 键入≥3 字可见下拉
- **状态**Done
- 执行记录 1单 query suggest API + debounce 下拉;禁止「使用我输入的」直通;须点选搜索结果
- 执行记录 2L1 payload 含 pickupConfirmed/deliveryConfirmed二级地址预填 formatted_address
### Step 12.65 MotherShip 登录后「继续」打通真实询价
- **输入**:一级已确认地址 + 货物;点击继续应 POST /api/quotes 并在二级右侧展示真实档位
- **输出**`buildQuoteRequestBodyFromLoggedIn`sidebar 接 QuoteDetail去掉预览假价
- **验证**:映射单测(截图样例 Fairhope→Brea / 2×250lbembed 继续后轮询出价
- **状态**Done
- 执行记录 1继续即 submitQuote跳过消歧弹窗pallet/weight/dims/option_id 映射
- 执行记录 2右侧按 quotes 列表展示真实 final_totalloading/error 态对齐
### Step 12.66 登录后报价侧栏对齐 + 价源 cookie
- **输入**:官网 Choose your carrier 信息密度;当前侧栏过简且价偏匿名 marketplace
- **输出**:侧栏承运商卡/保证加价/保障文案/ETAlogged-in storage 优先normalize 含 carrier 不去重挤掉
- **验证**session-logged-in-storage 单测;有账密 + `.rpa/mothership-logged-in-storage.json` 询价
- **状态**Done
- 执行记录 1侧栏展示已选徽章、时效、保障细节、保证送达加价、结账价联动
- 执行记录 2inline Direct 注入 customer/env 账密 ALS有登录态优先 logged-in storage cookie
### Step 12.67 有账密强制 dashboard rate-card 价源
- **输入**:官网 ABF/XPO/SAIA Direct ≠ 系统 TForce/Daylight Axel 档;侧栏需完整承运商卡
- **输出**`mothership-logged-in-quote.ts`;有账密跳过 Direct/L2 缓存;去重键含 carrier侧栏 Direct 文案
- **验证**`parseLoggedInRateCardText` + 多承运商 filter 单测;绑账密后 Continue 见 ABF 类承运商
- **状态**Done
- 执行记录 1`getQuote` 有账密走 dashboard DOM 抓 rate-cardsubmit 跳过 inline Direct 与匿名 L2
- 执行记录 2storage/filter/completeness 保留同档多承运商;侧栏提示价源与直连标签
### Step 12.68 客户账密读取说明与护栏
- **输入**:管理端已配账密仍疑“未读取”(实为已入库但旧路径未用)
- **输出**:按 `hasCustomerProviderCredential` 强制登录态;解密失败禁 Directembed 明示客户 ID
- **验证**DB `CUST_001` mothership 可解密;询价日志含 `source=customer`
- **状态**Done
- 执行记录 1核实账密在 `customer_provider_credential`CUST_001非 `.env MOTHERSHIP_*`
- 执行记录 2orchestrator/job-handler 护栏 + embed 绑定提示
### Step 12.69 登录态查价有头调试
- **输入**CUST_001 长时间无真实 rate-card需可视化定位卡住步骤
- **输出**`.env` `RPA_HEADED=true` / `RPA_HEADLESS=false` / `SLOW_MO=200`logged-in 分步日志
- **验证**Worker 重启后弹出 Chrome日志含 `logged-in step:`
- **状态**Done
- 执行记录 1有头 + slowMo失败打印当前 url成功前留屏 5s
- 执行记录 2步骤goto → login → createShipment → pickup/delivery → cargo → Continue → rate-card
### Step 12.70 关闭 Search any shipment 全局弹窗
- **输入**:有头调试卡在 dashboard 全局「Search any shipment」遮罩
- **输出**`dismissGlobalShipmentSearchModal` Escape/关闭钮;地址 Search 限定街道联想行
- **验证**:不再误点 `getByRole('button', { name: /Search/i })` 打开全局搜索
- **状态**Done
- 执行记录 1login/openCreate/pickAddress 前 dismisspickup 仅点 `Search {街道}` 联想行
- 执行记录 2日志 `关闭 Search any shipment 全局弹窗`
### Step 12.71 登录态附加服务与可提货时间写入 RPA
- **输入**:有头观测附加服务/日期时间未按一级表单选择执行
- **输出**:请求体透传 accessorials/ready_*DOM 点 `address-book-accessorials-option-*` + 日历/时刻
- **验证**:映射单测;有头日志含 `已选附加服务` / `已选可提货日|时刻`
- **状态**Done
- 执行记录 1QuoteRequestBody→NormalizedCargo→QuoteRequest 贯通cargo_hash 含登录态 extras
- 执行记录 2提货/派送分别开 trigger 勾选readyDate/readyTime 点 gridcell + option
### Step 12.72 按录制脚本纠正附加服务/日期填表顺序
- **输入**有头仍未勾选worker 日志跳过 accessorials旧进程录制顺序双地址后再选服务
- **输出**:对齐 `mothership-logged-in-*.js`trigger.first/nth(1) + option-* + 日期 Chevron + gridcell + `:00 PM`/`option`
- **验证**:重启 worker 后日志含 `pickup accessorials` / `已选可提货日`
- **状态**Done
- 执行记录 1录制顺序改写 applyAccessorials/applyReadyDateTime失败即抛超时
- 执行记录 2杀掉旧 worker 并有头重启加载新代码
### Step 12.73 按 140935 录制修正日期/时刻与派送附加
- **输入**`mothership-logged-in-20260715-140935.js`;时刻卡在日期后
- **输出**:日期钮匹配 `Wed, Jul 15th, 2026 Chevron`;时刻钮用截断名 `:00 AM/PM`;派送开 `Select Chevron down`
- **验证**:有头 Continue 能改到目标日 + option 时刻
- **状态**Done
- 执行记录 1摘录录制gridcell `Thu Jul 16`timeBtn `:00 AM` → option `12:00 PM`
- 执行记录 2delivery 不用 trigger.nth(1),改 Select Chevron downsearch-list-item 兜底
### Step 12.74 日历日格低对比度改 force 点击
- **输入**`gridcell name=Thu Jul 16 exact` wait visible 超时;截图非选中日呈浅灰
- **输出**`clickReadyDateInCalendar` attached+force多名称兜底采样日志
- **验证**:有头选 2026-07-16 不再 timeout
- **状态**Done
- 执行记录 1去掉 exact visible 等待;优先 aria-label/日号按钮
- 执行记录 2失败抛采样格名便于对录制漂移
### Step 12.75 时刻触发钮勿按目标 AM/PM 过滤
- **输入**:选 12:00 PM 时触发钮仍是 2:00 AM`/:\d{2}\s*PM/` 误点 `notification-item-shipment-rescheduled`
- **输出**`openReadyTimeDropdown` 排除 notification按当前触发钮 AM|PM 打开后再点 option
- **验证**:有头日志 `打开时刻下拉` + `已选可提货时刻 option="12:00 PM"`
- **状态**Done
- 执行记录 1触发钮定位与目标时刻解耦过滤 data-testid notification-*
- 执行记录 2点 after 收窄option 宽松匹配 12:00 PM
### Step 12.76 登录态多行货物对接 cargo-add-button
- **输入**:系统「添加另一行」后 MotherShip 仍只填一行;录制 140935 含第二行 Box
- **输出**`cargo_lines` 贯通请求RPA 循环点 `cargo-add-button` + `nth(i)` 填类型/数量/尺寸
- **验证**:映射单测 2 行;有头日志 `填货物行数=2` / `点击 cargo-add-button`
- **状态**Done
- 执行记录 1请求体/校验/QuoteRequest 增 cargo_lineshash 含行明细
- 执行记录 2fillCargo 按录制选 Pallet/Box 并填 nth(1)
### Step 12.77 登录态查价恢复无头浏览器
- **输入**:价格链路已可用;调试结束勿再弹出窗口
- **输出**`.env` `RPA_HEADLESS=true` / `RPA_HEADED=false`;重启 worker
- **验证**:询价不再弹出 Chrome 窗口
- **状态**Done
- 执行记录 1关闭 headed/slowMoworker 无头重启
- 执行记录 2保留登录态 dashboard 价源逻辑不变
### Step 12.78 Flock Freight 登录后查价界面录制
- **输入**:客户有 Flock 账密时官网为登录态查价 UI须人工 headed 录制,与匿名 get-a-quote 隔离
- **输出**`record-flock --logged-in``docs/rpa-flock-logged-in-sop.md``finalize-flock-logged-in-recording.ts``.env.example` 占位
- **验证**`vitest record-flock.test.ts`;产物前缀 `flock-logged-in-*` / storage 隔离于 `flock-storage.json`
- **状态**Done
- 执行记录 1`--logged-in` 独立 recordings/storage默认入口 loginSOP + ps1/sh清单提示 Let's build a quote
- 执行记录 2人工录制 `flock-logged-in-20260715-143321.js` + storage 落盘;附录 A 回填;脱敏 fixture原始 js 含明文勿提交
### Step 12.79 Flock 登录后 Quick 查价 UI创建报价
- **输入**:官网截图 + `flock-logged-in-20260715-143321.js`;有 Flock 账密客户看到登录后 Quick 表单
- **输出**`components/flock/flock-logged-in-quote-form.tsx`embed `providers.flock.has_password` 切换;`?flock_logged_in=1` 预览
- **验证**vitest 常量/映射embed-demo 切 Flock Tab 见「创建报价」Quick 全字段
- **状态**Done
- 执行记录 1还原地点类型/包装/货运等级/附加服务/车型/调度/保险;中文文案;提交映射现有 flock API
- 执行记录 2有账密或预览参数切登录态 UI无账密仍匿名 get-a-quote 表单
### Step 12.80 登录态查价 UI 视觉分层MotherShip / Flock
- **输入**:信息平铺、继续弱化、提示冗长、无进度;须保留官网字段布局
- **输出**`quote-form-chrome`MS/Flock 表单层级/留白/Tooltip/注意事项折叠;继续 CTA `#1890FF`;必填高亮
- **验证**`vitest quote-form-chrome` + mothership/flock 常量单测embed-demo 见进度与蓝 CTA
- **状态**Done
- 执行记录 1共用 chrome进度/竖线标题/Tooltip/折叠MS 提货缺失高亮「请填写提货地址」
- 执行记录 2Flock NMFC→注意事项折叠添加货物行悬停上浮不改全局 primary
### Step 12.81 Flock 地点类型附加选项按类别动态显示
- **输入**官网截图Residential/Construction/Trade Show/Church 等勾选项集合不同
- **输出**`FLOCK_LOCATION_ACCESSORIALS` + `getFlockLocationAccessorials`;提货/派送勾选随类型切换并清隐藏值
- **验证**vitest 矩阵断言;切 Residential 仅尾板+室内Trade Show 仅托盘车
- **状态**Done
- 执行记录 1补 Airport/Port/Trade Show/Residential去掉预约拆分住宅
- 执行记录 2切换类型时重置不可见字段单测覆盖 6 类关键矩阵
### Step 12.82 Flock Quick 官网 Tooltip 中文落地
- **输入**:地点类型 / 附加服务 / 车型 / 货运等级 / NMFC / 保险 / 堆叠提示截图
- **输出**:中文 tip 常量 + 加宽 `QuoteTipDefs`;ⓘ 悬停展示完整说明
- **验证**vitest 提示条文齐;界面悬停见地点定义与车型说明
- **状态**Done
- 执行记录 1地点 7 条定义、服务 5 条、车型 4 条、保险是/否、NMFC/堆叠条
- 执行记录 2Tooltip 支持 ReactNode + wide/below避免长文溢出
### Step 12.83 Flock 登录态货物硬限对齐 FLOCK_LIMITS
- **输入**:长 ≤636in / 宽 ≤102in / 高 ≤108in / 重 ≤45000lb与匿名表单同源
- **输出**`validateFlockLoggedInCargoLine`;提交校验 + input max整票合计重量上限
- **验证**vitest 超限文案同 `FLOCK_VALIDATION_MESSAGES`
- **状态**Done
- 执行记录 1挂 `lib/constants/flock-limits`;单行/整票重量双闸
- 执行记录 2字段 tip 展示上限8 用例全绿
### Step 12.84 Flock 登录态 Quick 查价链路打通 + 可视化进度
- **输入**截图参数Bags×2 / density / stackable…点「生成我的报价」须走真实 RPA 价
- **输出**`form_mode=logged_in_quick` 映射;件数 ≥1RPA 按包装/地点/等级/描述填表;`FlockQuoteProgress`
- **验证**vitest 映射/校验/选项文案embed 提交见进度与 quote_id
- **状态**Done
- 执行记录 1flock_input 扩展 + validation 放宽登录态件数logged-in-form 用真实选项
- 执行记录 2进度五步 + 用时/询价单号;停在填表/取价时提示检查 Worker/账密
### Step 12.85 Flock RPA 真实阶段进度(登录卡点可视化)
- **输入**`FLOCK_LOGIN_FAILED无法填写登录表单` 时用户看不出卡在哪
- **输出**Worker 回写 `__flock_progress`;轮询带 `rpa_stage`;失败钉在「登录」并中文 hintlogin 选择器兜底
- **验证**`rpa-progress` 单测;失败进度 `data-rpa-stage=login`
- **状态**Done
- 执行记录 1queued→lock→login→… 落库serializer/onProcessing 透传login 多候选定位+stall 截图
- 执行记录 2进度条改服务端阶段失败标红「卡在这里」+ mapFlockErrorToStage
### Step 12.86 Flock 有头浏览器观测(对齐 MotherShip
- **输入**:登录失败只有文字 hint无弹出浏览器可供肉眼看填表/登录
- **输出**`RPA_HEADED`/`RPA_HEADLESS` 统一解析Flock visual/session 有头+slowMo失败留屏 5s.env 开有头
- **验证**env 单测worker 日志 `headed=true`;查价弹 Chrome 窗
- **状态**Done
- 执行记录 1`isRpaHeaded`/`resolveRpaSlowMoMs`Flock 启动带 slowMo 与 dwell
- 执行记录 2`.env` 设 `RPA_HEADED=true`;需重启 worker 生效
### Step 12.87 Flock 两步登录填写客户账密
- **输入**:客户已配 Flock 账密;有头停在登录页不填(`source=customer` 已加载)
- **输出**:对齐录制 Email→Next→Password→Log in登录后进 home/New Quote/Quick
- **验证**两步登录单测worker 日志含「填写客户账密」
- **状态**Done
- 执行记录 1根因是先等 password当前屏仅邮箱导致永不 fill改两步流程
- 执行记录 2`openFlockLoggedInQuoteEntry`;重启 worker 后再测
### Step 12.88 Flock 登录态人工重录codegen
- **输入**:系统仍不清正确操作路径;需人工 headed 重录完整登录+Quick 出报价
- **输出**:新 `flock-logged-in-*.js` + `flock-logged-in-storage.json`;旧 storage 已备份
- **验证**:录制关闭后 `finalize-flock-logged-in-recording`;附录 A 回填
- **状态**Done
- 执行记录 1备份旧 storage启动 `record:flock -- --logged-in` 自 login 页
- 执行记录 2人工出报价 `TSM-TQ8Q`;脱敏 js提交改用 `quote-request-quick-form__submit_button`;附录 A 已更新
### Step 12.89 Flock 按录制有头可视化回放
- **输入**:人工录制已成功;系统需学习路径并可视化操作一遍
- **输出**`looksLoggedIn` 含 `/home``visual:flock-logged-in` 用客户账密回放 90248→15222 Bags
- **验证**:弹出 Chrome 完成登录+出报价;日志 OK + quotes
- **状态**Done
- 执行记录 1对齐录制路径与 submit testid新增 visual 脚本
- 执行记录 2New quote 用 `button/a:has-text`+force可视化回放出报价成功
### Step 12.90 Flock 附加服务/车型写入 RPA对齐标准截图
- **输入**:可视化附加服务 0 selected标准应为 Unloading + Box/Dry/Reefer
- **输出**UI 默认 unloading`additional_services`/`vehicle_types` 入队RPA chip 勾选
- **验证**:表单映射单测;回放日志含 Unloading Services
- **状态**Done
- 执行记录 1map/validation 透传;`applyLoggedInServicesAndVehicles`
- 执行记录 2visual SAMPLE 带 unloading + 三车型;默认 UI 勾选卸货服务
### Step 12.91 Flock 车型芯片误取消修复
- **输入**Unloading 已选,但 Box/Dry 被点关只剩冷藏车 → 温控红字、提交 disabled
- **输出**默认三车型跳过点击CheckCircle 判定选中;仅冷藏时自动恢复 Box/Dry
- **验证**:日志含「跳过点击(保留官网默认)」;提交可点
- **状态**Done
- 执行记录 1根因是芯片选中态误判导致「想打开却点关」
- 执行记录 2提交前等待 enabled兜底恢复厢式/干货
### Step 12.92 Flock Options Stackable 二次点击取消修复
- **输入**Stackable 勾上后又被取消checkbox + 文案各点一次)
- **输出**`ensureOptionToggle` 只交互一次并核对 isChecked
- **验证**:日志 `Options Stackable` 无双击;实测为开
- **状态**Done
- 执行记录 1去掉 getByText 二次点击
- 执行记录 2已是目标态则跳过
### Step 12.93 Flock 失败进度误钉「互斥锁」修复
- **输入**:官网已出真实价 / 浏览器闪退后重试,系统却显示卡在「获取 Flock 官网互斥锁」
- **输出**:失败优先 Worker `rpa_stage``mapFlockErrorToStage` 禁止默认 lock失败写进度不降级
- **验证**vitest rpa-progress未识别错误 matched=falseTarget closed→wait_quote
- **状态**Done
- 执行记录 1根因是失败映射默认 stage=lock且 UI 覆盖了 Worker 真实进度;闪退实为尝试失败关窗后 BullMQ 重试
- 执行记录 2Progress / job-handler / map 三处对齐;重启 worker 后重测
### Step 12.94 Flock `locator.count` 关窗竞态 + logged-in-form 修复
- **输入**:测其它参数时报 `locator.count: Target page, context or browser has been closed`
- **输出**`page-alive.ts` 安全 count`logged-in-form` 关窗友好返回stall/form/extract/visual-chain 接入
- **验证**vitest logged-in-form + page-alive 6/6esbuild 语法通过
- **状态**Done
- 执行记录 1根因是 browser.close 后仍 locator.count辅因 PowerShell 坏了 logged-in-form 语法后已从 transcript 重建
- 执行记录 2fill/submit 吞闭窗错误映射 BROWSER_CLOSED需重启 worker:rpa 再测换参查价
### Step 12.95 Flock 地点附属勾选 + 官网「仍在寻价」不重跑
- **输入**:住宅/农场下尾板室内未勾选;结果页 Hold tight 仍被当成失败并 BullMQ 重跑
- **输出**:类型切换默认勾选并下发 RPA`FLOCK_RATES_PENDING` 终态不重试
- **验证**vitest quote-extract pending + logged-in map 字段;重启 worker 后用住宅/农场景复测
- **状态**Done
- 执行记录 1切换地点类型→按矩阵默认勾选API/validation/RPA 写入 Liftgate/Inside/Pallet Jack
- 执行记录 2识别 Hold tight 页抽取参考号,标记不可重试;进度提示「勿重复提交」
### Step 12.96 Flock 登录态表单矩阵探针(只填不提交)
- **输入**:需验证各地点类型+附属勾选项能否在官网选中
- **输出**`probe:flock-form-matrix` + `logged-in-form-verify.ts`;截图/报告 `.rpa/flock-form-matrix/`
- **验证**:矩阵探针填表后回读 DOM修正 Liftgate/Pallet 官网文案 selector
- **状态**Done
- 执行记录 1新增矩阵脚本 38 组16 提货型+15 派送型+7 交叉),不点提交只截图对比
- 执行记录 2附属项改用「Liftgate Required for Pickup」等精确文案 + check();无附属类型跳过派送校验
### Step 12.97 Quick 小批量调度灰色项标注 + RPA 跳过
- **输入**:官网调度区灰色项不可选(小批量);系统须标注且禁止 RPA 硬点
- **输出**`FLOCK_PICKUP/DELIVERY_SERVICES` 对齐截图;提货前致电禁用;禁用项文案「小批量报价不可用」
- **验证**vitest 调度禁用矩阵RPA call-before 见 disabled 则跳过
- **状态**Done
- 执行记录 1指定提货时段改为 disabled提货前致电 Quick 强制关闭并不下发
- 执行记录 2派送「需要派送预约」保持可选其余窗口/已有预约/须到日灰色不可选
### Step 12.98 调度灰色项改为总重 >5000 动态启用
- **输入**:用户纠正——灰色按钮非永久禁用,整票总重 >5000 lb 后启用
- **输出**`schedulingWeightMinLb=5000``isFlockSchedulingOptionLocked`UI/RPA/单测按总重动态禁用
- **验证**vitest 总重 4000 锁定 / 5001 解锁;`call_before_pickup` 总重 ≤5000 强制 false
- **状态**Done
- 执行记录 1`FLOCK_LIMITS.schedulingWeightMinLb`;服务项 `weightLocked` 替代静态 `disabled`
- 执行记录 2表单实时汇总总重、自动回退锁定项说明文案改为「总重量超过 5000 lb 后启用」
### Step 12.99 灰色调度选项下级字段补充(对齐官网)
- **输入**:用户提供指定时段/已有预约/须到日截图,含子时间日期与预约致电
- **输出**:选中后展开 Start/End/Time/Date`must_arrive_by` 含「预约派送致电」API/校验透传
- **验证**vitest 映射透传调度子字段UI 总重 >5000 可选后展示下级表单
- **状态**Done
- 执行记录 1提货 during_window→起止时间have_appointment→时间派送 window→起止+日期have→时间+日期must_arrive→日期+致电
- 执行记录 2`mapFlockLoggedInToApiInput`/`validation`/`FlockQuoteInput` 透传调度扩展字段
### Step 12.100 地点类型对齐官网 12 项(剔除虚构 Limited Access
- **输入**:用户截图确认官网无 Grocery/Mini Storage/Prison/Utility
- **输出**:仅保留截图 12 项RPA 候选同步;矩阵交叉对替换真实类型
- **验证**vitest form constants矩阵不再生成虚构类型用例
- **状态**Done
- 执行记录 1删除 limited_grocery/mini_storage/prison/utilityMilitary 文案改为 Military Base
- 执行记录 2矩阵交叉 worship→school、military→other此前 10 个 Type 失败属测了不存在的选项
### Step 12.101 Pallet Jack 交叉「失败」实为校验误读派送侧
- **输入**:用户有头观察确认勾选正常
- **输出**:校验按类型矩阵分项启用;住宅无 Pallet 不读 nth(0);填表禁止误 uncheck 派送侧
- **验证**residential→farm/construction 不应再报 Pickup Pallet Jack
- **状态**Done
- 执行记录 1`logged-in-form-verify` 以 flags.palletJack 决定索引,避免提货无该项时误读派送
- 执行记录 2去掉住宅「强制 uncheck first Pallet」派送 Pallet 按提货是否有项选 nth
### Step 12.102 系统可测完善:调度 RPA + 清理脏矩阵 + 单测加固
- **输入**用户要在系统上检测注意事项12 地点/总重>5000 调度/Pallet 误读)一并落地
- **输出**RPA `applyLoggedInScheduling`矩阵报告剔除虚构类型validation 日期修复+调度透传测;地点=12 断言
- **验证**vitest form/rpa-options/validation嵌入 `/embed-demo?flock_logged_in=1` 手工测
- **状态**Done
- 执行记录 1扩展 LoggedInFormInput重量不足回退 standard_fcfsradio/时间日期/Call for delivery appointment
- 执行记录 2report 剔 grocery 等;单测 12 类型对齐 + Military Base + 调度文案权锁
### Step 12.103 Flock 中文文案对齐货运行业惯用译
- **输入**:用户反馈 Flock 直译不准,需符合本项目/LTL 语境
- **输出**:登录态选项与 tip、匿名 `FLOCK_UI`、校验提示统一为提货/派送、月台、盲运、干货挂车、可堆码等
- **验证**vitest flock validation + logged-in form constants
- **状态**Done
- 执行记录 1`flock-logged-in-quote-form` 地点/包装/附加服务/车型/Options tip 改为行业译法;与 MotherShip「受限通道」对齐
- 执行记录 2`ui-labels`/`flock-limits`/`validation` 取件投递→提货派送;单测断言同步
### Step 12.104 Flock 常用货件本地保存与搜索回填
- **输入**:用户要求「搜索常用货件」「货品描述」可保存名称后快速选用
- **输出**:按 customerId 的 localStorage 预设;描述旁「存为常用」;顶部搜索回填首行货物
- **验证**vitest saved-freight-presets嵌入登录态表单手工存选用
- **状态**Done
- 执行记录 1`lib/flock/saved-freight-presets.ts` + `flock-saved-freight-controls`;同名覆盖、删除、过滤
- 执行记录 2`FlockLoggedInQuoteForm`/`Widget` 透传 customerId保存含件数尺寸装载选项
### Step 12.105 管理端查价网站账密改为仅查看
- **输入**:用户反馈应直接查看账密,系统不提供修改能力
- **输出**GET 回传解密邮箱/密码;侧栏去掉保存/清除;新建客户仍可写入
- **验证**vitest admin view serialize管理端客户页手工查看
- **状态**Done
- 执行记录 1`listCustomerProviderCredentialsForAdmin` + 查看审计 `provider_credential_view`
- 执行记录 2`admin/customers` 侧栏只读显示/隐藏密码PUT 禁止 clear/已绑定后改密
### Step 12.106 RPA 切回无头(链路已确认)
- **输入**:用户确认可视化查价链路正常,要求隐藏浏览器窗口
- **输出**`.env` `RPA_HEADLESS=true`、`RPA_HEADED=false`;关掉 slowMo
- **验证**:重启 `worker:rpa` 后查价不再弹窗
- **状态**Done
- 执行记录 1本地 `.env` 有头调试开关关闭
- 执行记录 2需重启 rpa-worker 使环境变量生效
### Step 12.107 embed-demo 去掉宿主模拟说明文案
- **输入**:用户要求取消「模拟宿主系统内嵌查价组件…」描述
- **输出**`embed-demo-client.tsx` 删除该句;无账密预览提示保留
- **验证**:刷新 `/embed-demo` 不再显示该描述
- **状态**Done
- 执行记录 1删除加价规则说明段落首句
- 执行记录 2两侧均登录态时整段提示一并隐藏
### Step 12.108 登录态地址MotherShip 源校验 + RPA 选址修复
- **输入**选系统联想仍报「登录态地址未选中pickup要求联想须为 MotherShip 源
- **输出**`pickAddressSuggestion` Search 后 return + list-item`buildLoggedInAddressSearchQuery` 用结构化地址suggest 仅 Axel placeId + fetchPlace
- **验证**vitest candidates + mothership-logged-in-quote 9 用例通过
- **状态**Done
- 执行记录 1RPA 点 Search/`search-list-item` 后正确 return避免误抛 ADDRESS_SUGGESTION_NOT_FOUND
- 执行记录 2联想透传 Axel googlePlacesResultsUI 过滤无 option_id / 不可定位项
### Step 12.109 登录态 rate-card 超时Details/Inbox 卡点修复
- **输入**LA→SF 地址 90s 等不到 `rate-card`;诊断复现
- **输出**:关 Inbox「Got it」Details 点 Save & update / 回 Quotes剥离 floor/suite 搜索噪点
- **验证**`DIAG_HEADED=false` 诊断 exit 0拿到 ABF/XPO 等承运商价
- **状态**Done
- 执行记录 1有头一次成功无头卡在 Details+InboxrateCardCount=0
- 执行记录 2修复后无头 OK含 ABF Direct $744.57);失败落盘 `.rpa/diag/`
### Step 12.110 登录态填表加速 + 可视化复测(未来提货日)
- **输入**:价卡能出但地址/输入盲等过长;要求缩短并选非当天日期做有头观测
- **输出**PAUSE_* 替代长 wait联想 3.5sheaded slowMo 默认 60/80diag 默认 +3 天 12:00 PM
- **验证**:有头 `diag-mothership-logged-in-la-sf.ts` 人工观测
- **状态**Done
- 执行记录 1压缩选址/清障/Save 后盲等;避免填表阶段点 Ship 导航
- 执行记录 2启动有头可视化ready=+3d供人工看卡点
### Step 12.111 报价页已出但脚本不刮价Inbox 死循环假阴性
- **输入**:浏览器已到报价页,脚本长时间只打 `close Inbox Got it`,体感未取到价
- **输出**`rate-card` DOM count>0 即就绪Got it 会话限 2 次;刮价只清一次遮罩并立刻打 scraped 日志
- **验证**:诊断日志出现 `rate-card ready` + `scraped rates=` + `OK carriers=`
- **状态**Done
- 执行记录 1根源 ensureRateCardsReady 强依赖 isVisibleInbox 遮罩假阴性导致狂关遮罩
- 执行记录 2上一轮实际已 OK10 承运商);本步消除卡住体感并加速刮价出口
### Step 12.112 Brea→Fairhope 逐步计时 + 选址 30s 卡点
- **输入**:用户测 3 分钟无结果;指定 Brea→Fairhope / 7-20 12PM / Pallet 2×250×60×50×60
- **输出**step timing 日志;选址前关 Inbox 抽屉Search/list force clickrace 修复
- **验证**:无头诊断 exit 0选址各步 ≪30s输出逐步 ms
- **状态**Done
- 执行记录 1首轮选址各 ~31sInbox 挡点击空等默认 30s总 RPA ~76s
- 执行记录 2关抽屉+force 后复测:选址 0.9s/0.7sRPA 14.1swall 16.7s10 承运商
### Step 12.113 5 组车道批量计时
- **输入**:用户要求 5 组测试并记录每步用时
- **输出**`scripts/diag-mothership-timing-5cases.ts`;报告 `.rpa/diag/timing-5cases-*.md`
- **验证**5 组顺序跑完,汇总表 + 逐步 ms
- **状态**Done
- 执行记录 15 车道 Brea/LA/MA/Dallas/Atlanta统一 7-20 12PM Pallet 2×250
- 执行记录 24/5 OK14~22s/组Dallas→Chicago Continue 45s 后 rate-card 120s 超时
### Step 12.114 服务器上传包文档同步Flock + 登录查价大更新)
- **输入**:本地已落地 Flock / MotherShip 登录查价 / 客户承运商账密;需更新「上传到服务器」流程
- **输出**`deploy/RELEASE-NOTES.md`、`服务器更新说明.md`、`.env.bt-host-mysql.example`、打包校验路由、Dockerfile `NEXT_PUBLIC_FLOCK_*`
- **验证**:打包脚本含 flock/provider-credentials/mothership-suggestenv 模板含 Flock 开关
- **状态**Done
- 执行记录 1RELEASE-NOTES 2026-07-16 列出代码/迁移/env/验收全清单
- 执行记录 2compose build arg 透传 `NEXT_PUBLIC_FLOCK_POLL_TIMEOUT_MS`pack 校验新路由
### Step 12.122 Continue 后 grace 再等价,禁止加载期 patch 二级
- **输入**:报价加载中 RPA 误判缺二级必填,提前填 Details + Save&update 打断自然出价
- **输出**`waitForRatesAfterContinue` 25s grace仅非 loading 且仍 blocked 才 patch
- **验证**headed ×3 Albany→SF
- **状态**Done
- 执行记录 1Continue 后 grace 等待loading 期不 patch
- 执行记录 2×3 进行中(部分线路 grace 后仍 fallback patch
### Step 12.123 官网拒价文案如实返回前端
- **输入**MotherShip 显示附加服务/无价/必填提示时,系统不应只报超时或通用 RPA 错
- **输出**`portal-quote-messages.ts` 提取文案;`CARRIER_NO_CAPACITY` + 原文透传 `error_message`
- **验证**vitest portal-quote-messagesembed 侧栏显示官网原文
- **状态**Done
- 执行记录 1附加服务阻断立即抛No rates/required 在 patch 后或超时前抛原文
- 执行记录 2vitest 4/4 PASS
### Step 12.124 MotherShip 全部日期选择禁周末
- **输入**MotherShip 所有日期选择均不可选周六、周日
- **输出**`MothershipWeekdayDatePicker` 周末格子灰显不可点;一级/二级替换原生 dateAPI/RPA 仍 normalize
- **验证**vitest weekday-date-picker + constraints
- **状态**Done
- 执行记录 1自定义月历过去日/周六日 disabled 灰显,仅工作日可点
- 执行记录 2shipment/details 表单接入;不再选手动 snap 跳周一
### Step 12.125 Flock 提货日期禁周末
- **输入**Flock 提货日期与 MotherShip 一致,周六、周日不可选
- **输出**`pickup-date` 校验 + 默认 snap匿名/登录表单接入 `MothershipWeekdayDatePicker`
- **验证**vitest pickup-date + validation 周末 case
- **状态**Done
- 执行记录 1`pickupDateWeekend` 文案;`min/default` 拨到工作日API `validateFlockQuoteInput` 联动
- 执行记录 2`flock-quote-form`/`flock-logged-in-quote-form` 替换原生 date周六日灰显不可点
### Step 12.126 安全增量打包上传(省磁盘)
- **输入**:用户要求重新打包上传,避免占服务器磁盘、勿影响现网状态
- **输出**`pack-server-upload.ps1` 禁带校验;`chajia-code-20260716.zip`(仅代码)+ 全量 zipRELEASE-NOTES 安全铁律
- **验证**:包内无 .env/node_modules/.next关键 flock/登录/迁移路由齐全
- **状态**Done
- 执行记录 1打包 422 文件、代码约 2.15MBSAFETY ok
- 执行记录 2生成 code-only zip 推荐上传;说明勿覆盖 .env、勿导 SQL
### Step 12.127 DataGrip 手更库更新路径(替代自动 migrate
- **输入**:用户改用 DataGrip 更新数据库,不要安装脚本自动改库
- **输出**`datagrip-manual-migrate-20260716.sql` + `update-code-only-bt-host-mysql.sh`RELEASE-NOTES 方式 B
- **验证**SQL 含加列/建表;脚本串行 build 且跳过 migrate/seed
- **状态**Done
- 执行记录 1手更 SQL + resolve 标记说明,避免与 Prisma 历史冲突
- 执行记录 2文档改为推荐方式 B方式 A 保留为备选
### Step 12.128 明确:改库仅 SQL服务器零改库
- **输入**:用户确认不需要在服务器改库,只需 DataGrip 执行数据库语句
- **输出**SQL/脚本/RELEASE-NOTES 去掉必做 resolve服务器脚本仅代码更新
- **验证**:方式 B = DataGrip SQL + update-code-only禁止 install 自动 migrate
- **状态**Done
- 执行记录 1精简 datagrip SQL 与 update-code-only 提示
- 执行记录 2RELEASE-NOTES 方式 B 去掉 B3 resolve 步骤
### Step 12.129 上传未生效排查(须重建镜像 + .env
- **输入**:用户反馈上传代码且 DataGrip 已改库,但页面无新功能/无新参数
- **输出**:说明根因=未 rebuild新增 `verify-upload-bt-host-mysql.sh`
- **验证**自检磁盘路径、FLOCK_*、容器状态;指引跑 update-code-only
- **状态**Done
- 执行记录 1根因定位为 Docker 仍跑旧镜像 + 可能缺 .env
- 执行记录 2增加服务器一键自检脚本
### Step 12.130 服务器缺 update-code-only 脚本时的等价命令
- **输入**:服务器有 flock 源码与 FLOCK_ env但无 `deploy/update-code-only-bt-host-mysql.sh`
- **输出**:给出 compose 串行 build/up 等价命令(不改库)
- **验证**:用户可直接粘贴执行完成镜像重建
- **状态**Done
- 执行记录 1确认上传包未带齐 deploy 脚本
- 执行记录 2文档指引用 COMPOSE 串行重建替代缺失脚本
### Step 12.131 update-code-only 失败即中断
- **输入**:用户反馈更新流程缺少错误中断机制
- **输出**`update-code-only-bt-host-mysql.sh` 增加 trap ERR、前置校验、逐步 run_step、启动后 running 校验
- **验证**:缺文件/.env/build 失败时立即 exit不继续 up 半成品
- **状态**Done
- 执行记录 1preflight 校验 flock 源码与 FLOCK_*;禁 chrome channel
- 执行记录 2build/up 任一步失败打印步骤名并中断
### Step 12.132 重新打包上传(含 fail-fast 脚本)
- **输入**:用户要求重新打包并告知上传/更新步骤
- **输出**`chajia-code-20260717.zip`;包内含 update-code-only失败中断
- **验证**SAFETY ok关键 flock/脚本路径齐全
- **状态**Done
- 执行记录 1pack 425 文件 code≈2.16MB;生成 20260717 code zip
- 执行记录 2告知覆盖代码→跑 update-code-only→勿覆盖 .env/勿导库
### Step 12.133 修复 next build 类型错误Flock 常用货件下拉)
- **根因**`flock-saved-freight-controls.tsx` 从 react 导入 `type MouseEvent`,覆盖全局 DOM MouseEvent`document.addEventListener("mousedown", onDoc)` 参数类型不兼容(第 55/58 行)→ `npm run build` 失败
- **输出**:第 55 行改 `e: globalThis.MouseEvent`,最小改动
- **验证**ReadLints 无错;全仓仅此文件 import react MouseEvent其余下拉用全局类型不受影响
- **状态**Done
- 执行记录 1定位 next-app builder 5/5 报 overload no match at 58:44
- 执行记录 2仅改文档事件回调参数类型不动 React 合成事件用法
### Step 12.134 登录态查价入口直达 /ship修 Create a new shipment 超时)
- **根因**`openCreateShipment`(原 ~245 行)死等 `getByText(/Create a new shipment|创建新货件/)`;服务器未展开 Ship 菜单或未登录时 25s 超时
- **输出**:入口改为 `https://dashboard.mothership.com/ship`;登录检测含 `auth-email-input`;菜单路径仅兜底
- **验证**vitest `MOTHERSHIP_CREATE_SHIPMENT_URL`worker 日志含 `goto create shipment url=`
- **状态**Done
- 执行记录 1对齐录制 140935 直达 /ship会话过期也能识别登录表单
- 执行记录 2单测 9 通过SOP 入口 URL 已改
### Step 12.135 /ship 等 SPA 表单 + 本地可复现
- **根因**goto `/ship` 后立刻 `isVisible` 判空 → 掉进菜单文案兜底再 25s 超时(错误正则已含 Create shipment
- **输出**`waitForCreateShipmentForm`;登录后再强制 goto `/ship`;菜单短超时;失败抛 url+body
- **验证**`npx tsx scripts/probe-mothership-open-ship.ts` → OK 出价
- **状态**Done
- 执行记录 1本地用 logged-in storage 无头探通,~22s 出 9 档(含 ABF/XPO Direct
- 执行记录 2服务器须有有效 `.rpa/mothership-logged-in-storage.json` 或真实账密;空账密无法登录
### Step 12.136 再打包 20260717b含 /ship wait + 服务器缺 storage 说明)
- **输入**:服务器 `ls /app/.rpa/mothership-logged-in-storage.json` 不存在;日志命令误用字面量 `<rpa-worker>`
- **输出**`chajia-code-20260717b.zip`(含 Step 12.135storage 不进代码包SAFETY
- **验证**SAFETY ok包内含 `waitForCreateShipmentForm`
- **状态**Done
- 执行记录 1重新 pack + zip 0.73MB
- 执行记录 2告知需另拷 storage 或靠客户账密登录生成
### Step 12.137 登录竞态:等落定到 login/form 再填账密
- **根因**:服务器失败 body=Sign in to Mothershipurl=/logingoto /ship 后重定向前 `loginIfNeeded` 误跳过
- **输出**`waitForShipOrLogin`;登录失败抛 SESSION_EXPIRED仍停登录页时明确提示补 storage/账密
- **验证**:包 `chajia-code-20260717c.zip`;日志应有 `after /ship state=login` + `dashboard login email=`
- **状态**Done
- 执行记录 1修竞态 + 登录失败文案;重新打包 20260717c
- 执行记录 2部署后须确认客户账密或 docker cp storage 到 /app/.rpa/
### Step 12.138 更新 api调用文档鉴权 + 管理端 + 精简版)
- **输出**`api调用文档/api调用文档.md`、`api调用文档/api调用文档-第三方精简版.md`
- **验证**:对齐 HOST 两步接口、DB `chj_` API Key + env Token、客户管理/provider-credentials、cargo_type 枚举
- **状态**Done
- 执行记录 1完整版改为 API Key 优先;补 provider-credentials、其它渠道说明、公网测法
- 执行记录 2精简版同步鉴权与错误码第三方对接仍以两步宿主为主
### Step 12.139 嵌入登录区分登录类型 + Key 登录 4 路线门户
- **输入**:登录增加类型(账号密码 / API KeyKey 登录展示 MotherShip/Flock 各「免账号 + 登录后」共 4 路线;登录后路线点击填账密,有则回填邮箱 + 保存修改,无账密不可查价
- **输出**JWT 加 `lt` 声明;`/api/embed-demo/login` 支持 `api_key`(只填 Key 反查租户);`/me` 回传 `login_type`;新增 embed `/api/embed-demo/provider-credentials` GET/PUT可改密绕开管理端「首次不可改」`EmbeddedQuoteWidget` 加 `forcedProvider`/`hideProviderSwitch`;新增 `QuoteRouteHub``embed-demo-client` 按类型分流
- **验证**`npm run lint` 通过;`auth.test.ts` 4/4含 login_type 往返)
- **状态**Done
- 执行记录 1Key 与密码登录统一落 embed cookieraw Key 校验后即丢弃;密码登录页保持原状
- 执行记录 2登录后路线无账密时前端禁查价后端登录态入口本身已 fail-fastembed 改密与管理端「禁改」策略冲突已按用户需求在 embed 侧放开
### Step 12.140 冒烟验证:登录类型 + Key 4 路线门户
- **验证**:本地 `dev:start`;单测 13/13HTTP 密码/Key/`me`/账密 GET+PUT浏览器 Key 登录进 4 路线并进入 MotherShip 登录后表单
- **状态**Done
- 执行记录 1`demo-host-token` → CUST_001 + `login_type=api_key`;无效 Key 401空 Key 400账密 PUT 可改邮箱
- 执行记录 2UI 可见账号密码/API Key Tab、4 路线卡片、账密面板回填脱敏邮箱 + 登录后查价表单;冒烟误改邮箱已还原 `phoebeshen@aegrace.com`
### Step 12.141 承运商账密错误检测 + 明文回显
- **输入**:官网改密后未即时更新导致卡在登录页;需明确报错;账密明文展示便于核对
- **输出**`PROVIDER_LOGIN_FAILED` 错误码 + 统一用户文案MotherShip/Flock 登录失败映射embed GET/PUT 明文回传;账密面板 `type=text` 预填
- **验证**:相关 vitest 12+ 通过;`tsc` 通过Key 登录后 GET 返回明文 email/password
- **状态**Done
- 执行记录 1卡在登录页改抛 `PROVIDER_LOGIN_FAILED`不重试Flock 登录失败同映射
- 执行记录 2embed 账密面板明文展示并提示「改密须即时保存」;格式化错误文案强制引导用户确认账密准确性
### Step 12.142 对外报错统一中文(禁止英文技术串直出)
- **根因**`formatQuoteErrorMessage` / `resolveQuoteFailureMessage` 优先透出 RPA 原文Playwright Timeout、No rates found 等英文)
- **输出**`toChineseUserFacingMessage` 中文优先策略;官网英文阻断译中文;导航失败不再拼接英文 detail默认文案去技术运维术语
- **验证**quote-error-messages / portal-quote-messages / page-goto 相关单测通过;`tsc` 通过
- **状态**Done
- 执行记录 1合格中文保留已知英文短语翻译技术串按 error_code 回退中文默认
- 执行记录 2MotherShip portal `formatMothershipPortalQuoteMessage` 强制译中文后出给前端
### Step 12.143 管理端预警详情 Playwright 英文日志中文化
- **根因**`normalizeTechnicalReason` 未匹配时直出 `locator.waitFor: Timeout… getByTestId(...)`
- **输出**Playwright 等待超时 → 中文根因;识别 appointment/提货搜索等 testId其余英文技术串兜底中文化
- **验证**`alert-presentation.test.ts` 7/7`tsc` 通过
- **状态**Done
- 执行记录 1示例文案译为「自动化操作超时未能等到预约送货附加服务选项」
- 执行记录 2报价单 errorMessage 路径同样走 normalize避免详情仍出英文
### Step 12.144 打包上传 chajia-code-20260721
- **输入**:用户要求更新打包文件上传服务器
- **输出**`上传到服务器/` + `chajia-code-20260721.zip`0.74MB,仅代码)
- **验证**SAFETY ok含 quote-route-hub、embed provider-credentials、中文报错/预警映射
- **状态**Done
- 执行记录 1`pack-server-upload.ps1` files=428 codeMB=2.19;禁带 .env/node_modules/.next
- 执行记录 2增量更新用 `update-code-only-bt-host-mysql.sh`,勿覆盖服务器 `.env`
### Step 12.145 按 ccnew 体例重写第三方 API 对接文档
- **输入**:参考 `X:\work\ccnew\new\docs\api对接文档.md` 写法,生成本项目新对接文档
- **输出**`api调用文档/api对接文档.md`v1.0);精简版/完整版加入口指引
- **验证**:覆盖鉴权、两步流程、字段类型与约束、超时、错误码、枚举附录
- **状态**Done
- 执行记录 1对齐 ccnew 结构(文档说明/接入信息/推荐流程/分步接口/错误码/附录)
- 执行记录 2仅正式宿主两步接口为主异步三步/embed/管理端标为非推荐或不在范围
### Step 12.151 本地复现并修复登录态附加服务 appointment 超时
- **现象**Salem→San Carlos 询价失败只显示通用文案;库内最近失败多为 `address-book-accessorials-option-appointment` waitFor 8s
- **根因**`applyAccessorials` 硬等 option visible下拉未就绪/选项在 DOM 不可见即抛 Playwright 超时
- **输出**:加固打开下拉 + 别名/force/文案兜底;不可用时抛中文 `CARRIER_NO_CAPACITY`;探测脚本
- **验证**:本地 `probe-ms-logged-in-salem-sancarlos` 无预约 OK 10 档;`PROBE_APPOINTMENT=1` OK 9 档
- **状态**Done
- 执行记录 1无附加服务 43s 出 10 承运商;勾选 appointment 25s 出 9 档
- 执行记录 2错误文案单测覆盖 appointment Timeout → 自动化操作超时(含预约)
### Step 12.152 MotherShip 登录态 ×20 多线路探测 + 假阳性地址确认修复
- **现象**×20 中 #13 Boston→Philly 双侧尾板失败 `PAGE_LOAD_TIMEOUT`;页面仍停 `/ship`,派送占位未消失
- **根因**`tryPickOnce` 仅点 Search 即 return true`isAddressConfirmed` 不检查 delivery 占位且 input 有字即真
- **输出**占位双侧校验search-only 不算确认Continue 后未导航尽早抛中文地址未确认;用例地址改货运街址;`DIAG_ONLY`
- **验证**:补跑 #13报告汇总
- **状态**Done
- 执行记录 1×20 首跑 ok=19/20唯 #13 火车站联想假确认
- 执行记录 2修确认逻辑 + 改货运街址后补跑 #13 OK 27s tiers=7
### Step 12.153 Flock Freight 登录态 ×20 多线路探测
- **输入**:不同 ZIP/货物/地点类型/包装/附加服务;对齐 MotherShip ×20
- **输出**`scripts/diag-flock-logged-in-x20.ts`;报告 `.rpa/diag/flock-logged-in-x20-*.md``DIAG_ONLY`
- **验证**20 用例单测合法;无头批量跑汇总 ok/fail
- **状态**Done
- 执行记录 1脚本 + 用例校验单测落地CUST_001 登录态 DOM
- 执行记录 2首跑 ok=15/20调度 radio 降级;改 5 组用例补跑全 OK → 等效 20/20
### Step 12.154 MotherShip/Flock 选项互斥与易无价组合 — 选择时中文提示
- **输入**官网门户拒价住宅提货、Flock 仅冷藏须温控、调度重量锁、×20 易无价地点/包装
- **输出**`lib/mothership/option-compat.ts`、`lib/flock/option-compat.ts`;登录态表单灰禁/互斥自动切换/黄底提示;提交拦截 block
- **验证**option-compat 单测 + shipment/details 相关测通过
- **状态**Done
- 执行记录 1MS 禁住宅提货预约↔Amazon/住宅↔展会/CFS↔住宅互斥住宅派送建议尾板
- 执行记录 2Flock 冷藏×温控硬拦;施工/农场/units/仅 sprinter 警告;表单顶栏即时提示
### Step 12.155 打包 chajia-code-20260721c
- **输入**:用户要求打包更新服务器
- **输出**`上传到服务器/` + `chajia-code-20260721c.zip`0.75MB仅代码RELEASE-NOTES 更新
- **验证**SAFETY okfiles≈430禁带 .env/node_modules/.next
- **状态**Done
- 执行记录 1`pack-server-upload.ps1` codeMB=2.22zip 仅 `代码/*`
- 执行记录 2增量用 `update-code-only-bt-host-mysql.sh`,勿覆盖服务器 `.env`
### Step 12.156 账号密码登录与 API Key 统一两模块门户
- **输入**:密码登录需与 Key 登录同界面,只显示 2 个模块
- **输出**`embed-demo-client` 两种登录均走 `QuoteRouteHub`;门户仅 MotherShip / Flock 两卡,进入后切换「免账号 / 登录后」
- **验证**:无 Priority1 Tab密码与 Key 入口一致
- **状态**Done
- 执行记录 1去掉密码登录直进 EmbeddedQuoteWidget含三 Tab路径
- 执行记录 2模块内保留账密面板与查价闸门逻辑
### Step 12.157 打包 chajia-code-20260721d
- **输入**:用户要求再打一包(含两模块门户)
- **输出**`chajia-code-20260721d.zip`RELEASE-NOTES
- **验证**SAFETY ok
- **状态**Done
- 执行记录 1`pack-server-upload.ps1` + 仅代码 zip
- 执行记录 2增量 `update-code-only-bt-host-mysql.sh`,勿覆盖 `.env`
### Step 12.158 CC/宿主 iframe 直传 Key 跳过登录页
- **输入**:嵌到 CC 时应跳过登录表单,由宿主传类型与 Key
- **输出**`parseEmbedSsoParams``/embed-demo` 启动自动登录;成功后 strip URL生产 Cookie `SameSite=None`;对接文档 §2.0
- **验证**sso-params / auth 单测通过
- **状态**Done
- 执行记录 1去掉进页强制 logout优先 SSO → /me → 登录表单
- 执行记录 2推荐 `?login_type=api_key&api_key=...&embed=1`
### Step 12.159 对接文档 v1.2 调整 + 打包 20260722a
- **输入**:重新打包;调整 `api调用文档/api对接文档.md`
- **输出**:文档 §2.5 嵌入 SSO 完整化v1.2`chajia-code-20260722a.zip``上传到服务器/文档/api对接文档.md`
- **验证**SAFETY okzip≈0.75MB
- **状态**Done
- 执行记录 1厘清 OpenAPI 禁 URL Key vs iframe 服务端短时拼 src补 iframe/排错
- 执行记录 2包内含 SSO 直登代码 + 旁路附带对接文档
### Step 12.160 完整源码推送 Gitea团队可拉开发
- **输入**:本地未提交改动;目标 `https://git.shipro.ltd/nanchuanbeizi/chajia.git`
- **输出**:根 `README.md`;全量源码 commitremote `gitea` 推送 master
- **验证**:远端非空;无 `.env`;含 package-lock / prisma / .env.example / 测试与文档
- **状态**Done
- 执行记录 1`git add -A`gitignore 排除密钥与构建产物);补开发启动 README
- 执行记录 2push 至 Gitea供他人 clone 后按 README 启动