# implementation-plan > 功能实施步骤与执行日志。**v1.1 基线(Phase 0~9)已完成**;**v1.2 增量从 Phase 0 的 task-119 起按序执行**。 > 任务详情见 `docs/查价系统-工程任务拆解-v1.2.md`(当前权威版本)。 ## 里程碑 | 里程碑 | 条件 | 验收 | |--------|------|------| | M1 | Phase 0~4 基线 | 已完成 — `POST /quotes` L2 命中、幂等、校验 400 | | M2-v1.2 | Phase 0(119–120) + Phase 5.0~5 + Phase 10 | `RPA_MOCK_MODE=false` 下 `probe-rpa.ts` **连续 3 次 exit 0**,四档 `source_type=rpa` | | M3 | Phase 8 基线 | 已完成 — 宿主内嵌组件 + 管理端 | | M4 | Phase 9 基线 + task-133~135 | CI 全绿 + Go/No-Go probe 门禁 | ## v1.2 当前执行入口(Phase 0 增量) | 顺序 | Task | 状态 | 说明 | |------|------|------|------| | 1 | task-119 | **Done** | GD-15 RPA env 模板 + fail-fast | | 2 | task-120 | **Done** | docker `rpa_state` volume | | 3 | task-121~122 | 待执行 | RPA 录制前置 | | 4 | task-123~131 | **Done** | RPA v1.2 重构 | | 5 | task-132 | 已完成 | Bull Board | | 6 | task-133~135 | 待执行 | TC-601 + CI probe | > Phase 0 基线 072→073→070→071→097→030 与 119→120 均已完成;**下一入口 task-121**。 --- ## Phase 0 — 环境与底座 > Task:072 → 073 → **119 → 120** → 070 → 071 → 097 → 030 > 状态:**072–030、119–120 已完成**;**121 起待执行** ### Step 0.1 Docker Compose 四容器 - **Task**:task-072 - **输出**:`docker-compose.yml`(mysql/redis/next-app/rpa-worker + healthcheck) - **验证**:`docker compose up -d` 四容器 healthy - **状态**:Done - 执行记录 1:创建 docker-compose.yml,四服务 + mysql/redis healthcheck + rpa-worker shm_size 1gb - 执行记录 2:本机 Docker Desktop 未运行,容器启动待用户本地验证 ### Step 0.2 环境变量与 Dockerfile - **Task**:task-073 - **输出**:`.env.example` / `Dockerfile` / `Dockerfile.worker` - **验证**:`docker compose build` 无报错 - **状态**:Done - 执行记录 1:创建 .env.example(6 项必填变量)、Next standalone Dockerfile、worker Dockerfile - 执行记录 2:`npm run build` 通过;docker build 待 Docker 启动后验证 ### Step 0.3 v0.6 RPA 环境变量模板(v1.2 增量) - **Task**:task-119 - **输入**:PRD §8.2.1 GD-15;技术设计 §6.2 CHG-T03 - **输出**:`.env.example` 增 `MOTHERSHIP_QUOTE_URLS`、`RPA_STORAGE_STATE_PATH`、`RPA_SELECTOR_*`(8 项)、`RPA_USE_PATCHRIGHT` - **验证**:含全部 RPA 必填项;缺 `MOTHERSHIP_QUOTE_URLS` 时 rpa-worker fail-fast 退出 - **约束**:禁止默认 dashboard / `/quote` URL - **状态**:Done - 执行记录 1:lib/rpa/env.ts + assertRpaWorkerEnv;.env.example v0.6 模板;移除 dashboard 默认 URL - 执行记录 2:__tests__/lib/rpa/env.test.ts 6 用例通过;缺 URL 时 worker exit 1 ### Step 0.4 Docker RPA 持久化(v1.2 增量) - **Task**:task-120 - **输入**:技术设计 §6.1 §7.5 CHG-T04 - **输出**:`docker-compose.yml` rpa-worker 注入 RPA env + `rpa_state` volume → `/app/.rpa` - **验证**:`docker compose up -d` 后 `.rpa/mothership-storage.json` 跨容器重启保留 - **状态**:Done - 执行记录 1:rpa-worker 透传 MOTHERSHIP_QUOTE_URLS/RPA_* env;rpa_state volume 挂载 /app/.rpa - 执行记录 2:docker compose config 校验通过;.gitignore 忽略 .rpa/ ### Step 0.5 Prisma Schema - **Task**:task-070 - **输出**:`prisma/schema.prisma`(7 实体) - **验证**:`npx prisma validate` 零错误 - **状态**:Done - 执行记录 1:7 模型 SysUser/QuoteRecord/IdempotencyRecord/QuoteCacheMeta/MarkupConfig/AlertLog/AuditLog - 执行记录 2:`npx prisma validate` 通过;`npx prisma generate` 成功 ### Step 0.6 Migrate + Seed - **Task**:task-071 - **输出**:`prisma/migrations/` + `prisma/seed.ts`(admin_demo) - **验证**:migrate 成功;sys_user 有 1 行 admin - **状态**:Done - 执行记录 1:migration `20260616000000_init` + markup CHECK 约束;seed admin_demo/Demo@123 - 执行记录 2:migrate deploy + seed 已通过(MySQL 宿主机端口改为 3307 避开本机 3306 冲突) ### Step 0.7 Prisma 单例 - **Task**:task-097 - **输出**:`lib/prisma.ts` - **验证**:`prisma.$connect()` 成功 - **状态**:Done - 执行记录 1:lib/prisma.ts 全局单例,dev 热重载防多实例 - 执行记录 2:tsc 编译通过;$connect 待 MySQL 就绪后验证 ### Step 0.8 Redis 单例 - **Task**:task-030 - **输出**:`lib/redis.ts` - **验证**:`getRedis().ping()` → PONG - **状态**:Done - 执行记录 1:lib/redis.ts getRedis() 单例 + REDIS_URL 缺失抛错 + error 日志 - 执行记录 2:tsc 编译通过;ping 待 Redis 容器启动后验证 --- ## Phase 1 — 基础设施与缓存 > Task:096 → 031 → 032 → 033 → 034 → 035 → 035-ip → 036 ### Step 1.1 统一响应包络 - **Task**:task-096 - **输出**:`lib/response.ts`(ok/fail) - **验证**:Vitest 覆盖 code/message/data - **状态**:Done - 执行记录 1:lib/response.ts 实现 ok/fail + Cache-Control: no-store - 执行记录 2:__tests__/lib/response.test.ts 2 用例通过 ### Step 1.2 L1 幂等缓存 - **Task**:task-031 - **输出**:`modules/cache/redis-cache.ts` getL1/setL1 - **验证**:SETNX + TTL 86400s - **状态**:Done - 执行记录 1:setL1 SETNX 不覆盖;getL1 JSON 反序列化 - 执行记录 2:单测验证 TTL=86400 ### Step 1.3 L2 热缓存 - **Task**:task-032 - **输出**:getL2/setL2 - **验证**:TTL 180~210s jitter - **状态**:Done - 执行记录 1:setL2 写入 quote:{cargo_hash},TTL 180+random(0~30) - 执行记录 2:单测 20 次采样 TTL 均在 180~210 区间 ### Step 1.4 L3 Stale 缓存 - **Task**:task-033 - **输出**:getL3/setL3 - **验证**:TTL 1800s - **状态**:Done - 执行记录 1:setL3 写入 stale:{cargo_hash},TTL 1800s - 执行记录 2:单测验证 TTL=1800 ### Step 1.5 缓存击穿锁 - **Task**:task-034 - **输出**:acquireLock/releaseLock/waitAndGetL2 - **验证**:SETNX lock:quote:{hash} TTL 5s - **状态**:Done - 执行记录 1:acquireLock SETNX + releaseLock DEL + waitAndGetL2 2s - 执行记录 2:单测验证重复 acquire 返回 false ### Step 1.6 客户限流 - **Task**:task-035 - **输出**:`modules/cache/rate-limiter.ts` - **验证**:60 次/分钟/customer - **状态**:Done - 执行记录 1:checkRateLimit INCR+EXPIRE 60s,超限 RATE_LIMITED - 执行记录 2:单测 60 通过 + 第 61 拒绝 ### Step 1.7 IP 限流 - **Task**:task-035-ip - **输出**:`modules/cache/ip-rate-limiter.ts` - **验证**:1000 次/分钟/IP,超限封禁 10 分钟 - **状态**:Done - 执行记录 1:checkIpRateLimit ban:ip:{ip} + ratelimit:ip:{ip} - 执行记录 2:单测 1000 通过 + 超限 banned=true ### Step 1.8 RPA 熔断 - **Task**:task-036 - **输出**:`modules/cache/circuit-breaker.ts` - **验证**:连续 3 次失败 open,TTL 600s - **状态**:Done - 执行记录 1:isCircuitOpen/openCircuit/closeCircuit/recordRpaFailure - 执行记录 2:单测 3 次失败熔断 + closeCircuit 清除 --- ## Phase 2 — 鉴权 > Task:001 → 002 → 003 → 004 → 005 → 098 ### Step 2.1 JWT 签发/校验 - **Task**:task-001 ~ task-003 - **输出**:`modules/auth/`(宿主 token + 管理员 JWT) - **验证**:有效/过期/伪造 token 分支 - **状态**:Done - 执行记录 1:jwt.ts sign/verify/decode + login route + middleware 路由保护 - 执行记录 2:jwt.test.ts 4 用例通过;tsc 零错误 ### Step 2.2 RBAC 中间件 - **Task**:task-004, task-005 - **输出**:`middleware.ts` 鉴权 + 权限校验 - **验证**:越权 403;权限 `alert:read` `rpa:operate` `pricing:markup:write` - **状态**:Done - 执行记录 1:service-token.ts 租户绑定 + rbac.ts requirePermission - 执行记录 2:service-token/rbac 单测 7 用例通过 ### Step 2.3 限流 - **Task**:task-098 - **输出**:令牌桶限流 - **验证**:超限返回 RATE_LIMITED - **状态**:Done - 执行记录 1:lib/gateway-retry.ts 5xx 单次重试 → 503 INTERNAL_ERROR - 执行记录 2:gateway-retry.test.ts 3 用例通过 --- ## Phase 3 — 领域逻辑 > Task:040~044 → 010~014, 092, 095, 109, 111, 019 ### Step 3.1 加价引擎 - **Task**:task-040, task-041, task-044 - **输出**:`modules/pricing/engine.ts` - **验证**:0~30% 边界;仅 raw_freight 加价 - **状态**:Done - 执行记录 1:lib/math.ts roundHalfUp + engine applyMarkup/getMarkupPercent/applyMarkupToQuotes - 执行记录 2:math.test.ts + engine.test.ts 11 用例通过;tsc 零错误 ### Step 3.2 入参校验 - **Task**:task-010, task-011 - **输出**:`modules/quote/validation.ts` - **验证**:Mothership 官方限制;地址 place_id - **状态**:Done - 执行记录 1:validation.ts MOTHERSHIP_LIMITS + zod 校验 + unit-converter + cargo-hash - 执行记录 2:validation/unit-converter/cargo-hash 单测 TC-201~204 通过 ### Step 3.3 幂等模块 - **Task**:task-012 - **输出**:`modules/quote/idempotency.ts` - **验证**:24h 同 request_id 返回同一 quote_id - **状态**:Done - 执行记录 1:idempotency.ts Redis L1 + DB 双保险 + P2002 冲突处理 - 执行记录 2:idempotency.test.ts 5 用例通过 ### Step 3.4 询价编排器 - **Task**:task-013, task-014, task-019 - **输出**:`modules/quote/orchestrator.ts` - **验证**:L1→L2→RPA 状态机完整 - **状态**:Done - 执行记录 1:orchestrator submitQuote + quote-id 生成 + rpa-queue 占位入队 - 执行记录 2:orchestrator.test.ts TC-104/105 + processing 路径 3 用例通过 ### Step 3.5 Fallback 编排 - **Task**:task-092 - **输出**:`modules/quote/fallback-orchestrator.ts` - **验证**:RPA 失败 → L3 → STALE_FALLBACK 预警 - **状态**:Done - 执行记录 1:handleRpaFailure L3 降级/失败分支 + alert_log 写入 - 执行记录 2:fallback-orchestrator.test.ts TC-205/206 通过 ### Step 3.6 置信度与四档结构 - **Task**:task-095, task-109, task-111 - **输出**:confidence_score + 4 档 quotes_json 结构 - **验证**:TC-102 四档验收 - **状态**:Done - 执行记录 1:confidence.ts + quote-completeness assertFourTiers + quote-id P2002 audit - 执行记录 2:confidence/quote-completeness/quote-id 单测 13 用例通过;npm test 74/74 --- ## Phase 4 — API 层(里程碑 M1) > Task:015 → 016 → 017 → 042 → 043 → 018 ### Step 4.1 POST /api/quotes - **Task**:task-015 - **输出**:`app/api/quotes/route.ts` - **验证**:TC-104 cache 命中、TC-105 幂等、TC-201~204 校验 400 - **状态**:Done - 执行记录 1:route.ts 薄层:鉴权 + 限流 + orchestrator + ValidationError→400 - 执行记录 2:quotes-routes.test.ts 2 用例通过;npm test 91/91 ### Step 4.2 GET /api/quotes/{quote_id} - **Task**:task-016 - **输出**:`app/api/quotes/[quote_id]/route.ts` - **验证**:轮询 processing→done;404 QUOTE_NOT_FOUND - **状态**:Done - 执行记录 1:quote-serializer.ts expired/confidence 格式化 + 越权 403 - 执行记录 2:quote-serializer + quotes-routes 单测 TC-501/404 通过 ### Step 4.3 GET /api/quotes/history - **Task**:task-017 - **输出**:`app/api/quotes/history/route.ts` - **验证**:分页;customer_id 隔离 - **状态**:Done - 执行记录 1:history/route.ts 强制 page+size + customer_id 过滤 - 执行记录 2:TC-506 缺分页→400 单测通过 ### Step 4.4 PUT /api/markup-configs/{customer_id} - **Task**:task-042, task-043 - **输出**:`app/api/markup-configs/[customer_id]/route.ts` - **验证**:权限 + 0~30% 校验 - **状态**:Done - 执行记录 1:GET list + PUT upsert;RBAC pricing:markup:write + 0~30 校验 - 执行记录 2:TC-502/505 单测通过;isKnownCustomer 客户存在校验 ### Step 4.5 询价超时处理 - **Task**:task-018 - **输出**:processing 30s 超时 → QUOTE_TIMEOUT - **验证**:TC 超时场景 - **状态**:Done - 执行记录 1:timeout-sweeper.ts 10s 扫描;L3→stale / 无L3→QUOTE_TIMEOUT - 执行记录 2:timeout-sweeper.test.ts 4 用例通过;worker:scheduler 脚本已加 --- ## Phase 5 — RPA > Task:020~029 → 110 → 093, 099, 100 ### Step 5.1 BullMQ 队列 - **Task**:task-020 ~ task-022 - **输出**:`workers/rpa/queue.ts` + job 定义 - **验证**:入队/消费/重试策略 - **状态**:Done - 执行记录 1:QuoteProvider 接口 + BullMQ quote-rpa 队列 + session-manager 登录态 - 执行记录 2:rpa-queue 入队 + worker index 消费/熔断暂停;103 单测通过 ### Step 5.2 Playwright RPA Provider - **Task**:task-023 ~ task-028 - **输出**:`workers/rpa/mothership-provider.ts` - **验证**:地址联想点选 + 四档抓取 - **状态**:Done - 执行记录 1:mothership.ts preCheck/fillAddress/fillCargo/scrapeQuotes/validateResult + error-mapper - 执行记录 2:validateResult + mock 四档单测通过;RPA_MOCK_MODE 可本地联调 ### Step 5.3 RPA 管理 API - **Task**:task-029, task-093, task-099, task-100, task-110 - **输出**:Worker 状态/暂停/恢复 API - **验证**:RPA_CAPTCHA 暂停 Worker - **状态**:Done - 执行记录 1:job-handler 成功写 L2/L3/落库/偏差检测;失败走 fallback;管理 API 三路由 - 执行记录 2:GET /api/rpa/status + pause + manual-fallback;middleware 管理员路由已放行 --- ## Phase 6 — 预警 / 审计 > Task:050~057 → 094 → 105 ### Step 6.1 预警服务 - **Task**:task-050 ~ task-055 - **输出**:`modules/alert/service.ts` - **验证**:4 种预警类型写入 alert_log - **状态**:Done - 执行记录 1:writeAlert 统一写入 + deviation/fallback/job-handler/sweeper 重构 - 执行记录 2:8 类预警(含 STRUCT_CHANGE/SESSION_EXPIRED/MANUAL_FALLBACK);114 单测通过 ### Step 6.2 预警 API - **Task**:task-056, task-057 - **输出**:`app/api/alerts/` 列表 + resolve - **验证**:管理员 RBAC;分页 - **状态**:Done - 执行记录 1:GET /api/alerts 分页+type/status 筛选;POST resolve 更新 resolved - 执行记录 2:alerts-routes.test.ts 6 用例通过;middleware 已放行 /api/alerts ### Step 6.3 审计日志 - **Task**:task-094, task-105 - **输出**:`modules/audit/` + AuditLog 写入 - **验证**:高危操作可追溯 - **状态**:Done - 执行记录 1:writeAudit + recordSecurityEvent;越权/注入/IP封禁写 SECURITY - 执行记录 2:audit/service.test.ts 3 用例;quote-id/manual-fallback 已迁移 --- ## Phase 7 — 观测指标 > Task:101 → 102 ### Step 7.1 三率指标 - **Task**:task-101, task-102 - **输出**:API/RPA/realtime rate 采集 - **验证**:GD-4 指标可查询 - **状态**:Done - 执行记录 1:modules/metrics/collector.ts Redis 计数器 + orchestrator/RPA/fallback/sweeper 埋点;GET /api/metrics/dashboard - 执行记录 2:collector 7 用例 + dashboard 2 用例通过;npm test 123/123;middleware 已放行 /api/metrics --- ## Phase 8 — 前端 > Task:060~069 → 106 → 063, 064, 079, 103, 104, 080 ### Step 8.1 宿主内嵌查价组件 - **Task**:task-106(替代 075~077) - **输出**:内嵌组件(地址联想、托盘数、四档展示、3min 倒计时) - **验证**:异步轮询;L2 命中无 RPA - **状态**:Done - 执行记录 1:Tailwind+Geist+UI 组件库;QuoteForm/QuoteCard/CountdownTimer/QuoteResultPanel;use-quote-polling 退避轮询;EmbeddedQuoteWidget + /embed-demo - 执行记录 2:pollQuoteUntilDone 3 用例通过;npm test 126/126;next build 20 路由全绿 ### Step 8.2 管理端 - **Task**:task-060 ~ task-069, task-063, task-064, task-079, task-103, task-104, task-080 - **输出**:`app/admin/` 预警中心 + RPA/开发进度 - **验证**:管理员登录;预警处理;文案全中文 - **状态**:Done - 执行记录 1:/login /dashboard /admin/alerts /admin/rpa /admin/dashboard;AdminLayout+JWT 本地存储;403/not-found 错误页 - 执行记录 2:预警列表分页+标记已处理+RPA 暂停入口;指标看板 GD-4 四卡;仅 admin 入口无 customer 卡片 --- ## Phase 9 — 测试 / CI > Task:081~118 → 091 ### Step 9.1 单元测试 - **Task**:task-081 ~ task-091 - **输出**:Vitest 覆盖核心模块 - **验证**:覆盖率达标 - **状态**:Done - 执行记录 1:task-081~087 单测已覆盖 engine/validation/cargo-hash/idempotency/deviation;补 surcharge=0;test:unit 脚本 104 用例 - 执行记录 2:npm run test:unit 全绿;engine TC-103 markup=32/367 精确到分 ### Step 9.2 集成 / E2E - **Task**:task-112 ~ task-118 - **输出**:API 集成测试 + Playwright E2E - **验证**:TC 全集通过 - **状态**:Done - 执行记录 1:新增 quotes.integration/security/l2-expire/cache-consistency/rpa-admin/recalc-from-db;load-tests 三脚本;go-no-go-check.ts - 执行记录 2:test:integration 33 + E2E 7 全绿;go-no-go --mock PASS ### Step 9.3 CI 流水线 - **Task**:task-091 - **输出**:`.github/workflows/` 或等效 CI - **验证**:lint + test + build 全绿 - **状态**:Done - 执行记录 1:ci.yml(lint+test:unit+test:integration+build);e2e-nightly.yml(E2E+k6+go-no-go) - 执行记录 2:npm test 145/145;lint 零错误;playwright 7/7;build 20 路由全绿 --- ## Dev Infra — Docker 绕过 / WSL 原生 ### Step Dev.1 本地开发基础设施修复 - **输出**:`scripts/dev/wsl-infra.sh`、`bootstrap-wsl-infra.ps1`、`diagnose-docker.ps1`;`DEV_INFRA_MODE=native` - **验证**:`npm run dev:infra:setup` + `npm run dev:start` 全绿 - **状态**:Done - 执行记录 1:根因 Docker Desktop WSL IPC 死锁(engine_500);移除有害 SwitchDaemon;WSL Ubuntu-22.04 原生 mysql/redis;动态解析 WSL IP 供 Windows 连接 - 执行记录 2:`dev:start` migrate/seed/smoke/next:3000 全绿;`dev:diagnose-docker` 确认 engine_500 并给出 WSL 路径 ### Step Dev.2 真实 RPA 验收(关闭 Mock) - **输出**:PRD v0.5 匿名查价条款;`.env` `RPA_MOCK_MODE=false`;`scripts/smoke-real-rpa.ts` - **验证**:`npm run smoke:rpa` 返回 4 档 `source_type=rpa` - **状态**:Done - 执行记录 1:PRD §8.2 + 技术设计 §7.2 补充匿名查价;关闭 Mock;修复 session-manager 先判登录页再要凭据 - 执行记录 2:实测 `www.mothership.com/quote` 404;默认 URL 改 `dashboard.mothership.com`;真实冒烟需配置 MOTHERSHIP 凭据 --- ## Phase 10 — RPA v1.2(PRD v0.6 / 技术设计 v1.2) > Task:119 → 120 → 121 → 122 → 123~131 → 132 → 133~135 ### Step 10.0 环境与 Docker RPA 持久化 - **Task**:task-119, task-120(**与 Phase 0 Step 0.3–0.4 同任务**) - **输出**:`.env.example` RPA env;`docker-compose.yml` rpa_state volume - **验证**:缺 `MOTHERSHIP_QUOTE_URLS` fail-fast;storageState 跨重启保留 - **状态**:Done(见 Phase 0 Step 0.3–0.4) ### Step 10.1 RPA 录制前置 - **Task**:task-121, task-122 - **输出**:`scripts/record-mothership.sh`;`docs/rpa-recording-sop.md` - **验证**:headed 录制产出 URL + 8 个 selector env - **状态**:Done - 执行记录 1:新增 `record-mothership.sh`(codegen headed、GD-15 URL 拦截、storageState 读写)及 `docs/rpa-recording-sop.md`(LA→Dallas 样例 + 8 selector 映射表 + checklist) - 执行记录 2:`vitest __tests__/scripts/record-mothership.test.ts` 3/3 通过 ### Step 10.2 RPA v1.2 重构 - **Task**:task-123 ~ task-131 - **输出**:`quote-page-adapter.ts`;storageState 生命周期;selector env 化;错误分类 v0.6;probe 3/3 - **验证**:`bash scripts/probe-rpa-3x.sh` 连续 3 次 exit 0 - **状态**:Done - 执行记录 1:QuotePageAdapter 多 URL 探测 + storageState + selector env 化 + QUOTE_ENTRY_UNAVAILABLE + probe 3x 脚本 - 执行记录 2:单测 adapter/storage/fallback-entry 通过;`RPA_MOCK_MODE=true` probe 3/3 exit 0 ### Step 10.3 Bull Board 队列页 - **Task**:task-132 - **输出**:`app/admin/queues/page.tsx` - **验证**:admin 可见 quote-rpa 队列;非 admin 403 - **状态**:Done - 执行记录 1:Hono + Bull Board 挂载 `/api/admin/queues`;stats API + 中文摘要卡片;iframe cookie 鉴权;导航入口 - 执行记录 2:`admin-queues.test.ts` 4/4 通过;`npm run build` 含 `/admin/queues` 路由 ### Step 10.4 验收与 CI probe 门禁 - **Task**:task-133, task-134, task-135 - **输出**:TC-601 修订;go-no-go probe 3/3;e2e-nightly probe 步骤 - **验证**:Go-No-Go 含 probe 门禁;TC-601 items=4 - **状态**:Done - 执行记录 1:`lib/rpa/probe-validation.ts` + `run-probe.ts`;`rpa-probe.test.ts`;e2e TC-601 items=4 断言;go-no-go probe 3/3 - 执行记录 2:`e2e-nightly` 增 probe job(secrets 缺失跳过);`go-no-go --mock` PASS;integration 5 用例通过 ### Step 10.5 本地 RPA 环境未配置导致查价超时 - **根因**:`.env` 缺 `MOTHERSHIP_QUOTE_URLS` + 8 项 `RPA_SELECTOR_*` → worker fail-fast 未消费队列 → 前端 30s 轮询超时 - **输出**:`scripts/dev/lib.ps1` `Assert-DevWorkerHealthy`;`start.ps1` 启动后校验 worker 存活 - **验证**:缺 env 时 `npm run dev:start` 明确报错而非静默超时 - **状态**:Done - 执行记录 1:日志证实 `QTE_20260617_0004` 入队后 worker 因 `MOTHERSHIP_QUOTE_URLS` 空而启动失败 - 执行记录 2:`dev:start` 增 worker 存活/日志 fail-fast 检测,避免「查询超时」误导 ### Step 10.6 Windows 录制脚本(无需 bash) - **输出**:`scripts/record-mothership.ts` + `record-mothership.ps1`;`npm run record:mothership` - **验证**:`vitest record-mothership.test.ts` 通过;`--help` 可执行 - **状态**:Done - 执行记录 1:根因 Windows 无 `/bin/bash`;新增 tsx 跨平台录制入口 - 执行记录 2:SOP 补充 Windows 命令;保留 `.sh` 供 Linux/macOS ### Step 10.7 MotherShip 地址歧义:先选后报价 - **需求**:提货/派送在 MotherShip 有多精确坐标时禁止 RPA 自动择一;先返回候选,用户确认后再询价 - **输出**:`POST /api/addresses/mothership-candidates`;`AddressDisambiguationModal`;询价校验 `selected_from_mothership` - **验证**:Dallas 75201 返回 4 候选;未确认 MotherShip 地址时 `POST /api/quotes` 拒绝 - **状态**:Done - 执行记录 1:新增候选 API + mock 目录;RPA `fillAddressField` 仅点用户 `mothership_display_label` - 执行记录 2:`embedded-quote-widget` 多选弹窗;`validation`/`cargo-hash` 测试通过 ### Step 10.8 配置 .env + 真实 RPA 地址列表抓取 - **需求**:`.env` 按录制 `112133` 填齐;`RPA_MOCK_MODE=false` 时禁止候选 API 回退 mock catalog - **输出**:`lib/rpa/locator.ts`;`workers/rpa/address-candidates.ts`;`.env` selector 全量 - **验证**:`mothership-candidates.test.ts` 断言走 `fetchMothershipAddressCandidates` - **状态**:Done - 执行记录 1:新增 `pageLocator` 解析 role/text/placeholder;首页 widget 分区 selector - 执行记录 2:`.env` 写入 `MOTHERSHIP_QUOTE_URLS` + 8 项 selector + 货物 placeholder ### Step 10.9 重录 115059/115541 更新 .env - **依据**:`mothership-20260617-115059.js`(四档 UI)、`115541.js`(地址分区) - **变更**:`最低价格` 替代 `最优性价比`;分区改 `text=从何处取货/送货至`;增 `CARGO_SECTION`/`TRANSIT` - **状态**:Done - 执行记录 1:`.env` / `.env.example` 按新录制同步 - 执行记录 2:`fillCargo` 支持 `RPA_SELECTOR_CARGO_SECTION` 展开货运详情 ### Step 10.10 worker 启动 fail-fast:FASTEST 改可选 - **根因**:`.env` 中 `RPA_SELECTOR_FASTEST_OPTION=` 空;`assertRpaSelectorsEnv` 仍要求 8 项全填 - **修复**:`RPA_REQUIRED_SELECTOR_ENV_KEYS`(7+PRICE);dev 后台进程预加载 `.env` - **状态**:Done - 执行记录 1:`selectors.ts` FASTEST 可空;115059 录制无独立 fastest 档 - 执行记录 2:`lib.ps1` Start-DevBackgroundProcess 注入根 `.env` ### Step 10.11 dev:start 误报 worker 启动失败 - **根因 1**:`infra.env` 覆盖 `.env` 注入;**根因 2**:健康检查读日志 tail 含历史 FASTEST 报错(worker 已启动) - **修复**:`.env`+`infra.env` 顺序合并;启动前清空 log;仅解析最近一次 run - **状态**:Done - 执行记录 1:`Assert-DevWorkerHealthy` 用 `Get-DevLogLatestRunText` - 执行记录 2:`npm run dev:start` exit 0,worker-rpa pid 存活 ### Step 10.12 embed 查价「令牌无效或已过期」 - **根因**:`POST /api/addresses/mothership-candidates` 未纳入 `isHostRoute`,middleware 用 JWT 校验 `demo-host-token` - **修复**:`middleware.ts` 增加 `pathname.startsWith("/api/addresses")` - **状态**:Done - 执行记录 1:点击获取报价先调地址候选 API,非 `/api/quotes` 路径 - 执行记录 2:宿主 Service Token 与 quotes 路由同等鉴权 ### Step 10.22 填表 click 假通过 + RPA 超时窗口 - **根因**:`openAddressSide` 吞掉 click 异常;派送误命中不可编辑提货框;30s 窗口不足 - **修复**:`clickStreetInput` 可编辑校验;派送优先 `:focus` + 末位可编辑框;超时 60s - **状态**:Done - 执行记录 1:`address-side.ts` 禁止吞 click;`address-suggestions` scroll+超时;pipeline/mothership 阶段日志 - 执行记录 2:`QUOTE_TIMEOUT_MS`/`POLL_TIMEOUT_MS`/`RPA_JOB_TIMEOUT_MS` 对齐 60/60/55s ### Step 10.24 fillAddress 8s 卡死:input:focus 误匹配 - **根因**:`focusedStreetInput` 全页 `:focus` 命中不可点击元素;跳过「先点分区」与录制不一致 - **修复**:widget 内 `getByRole(textbox)`;始终先点分区;click 失败 force 回退 - **状态**:Done - 执行记录 1:`address-side.ts` 移除 `:focus`;对齐 115541 双点送货至 - 执行记录 2:`address-suggestions` click force 回退;单测通过 ### Step 10.23 前端轮询 30s 提前超时 - **根因**:`pollQuoteUntilDone` 固定 15 次 × 2s ≈ 30s,与 `POLL_TIMEOUT_MS=60s` 进度条不一致 - **修复**:轮询改 wall-clock `POLL_TIMEOUT_MS`;embed 超时文案对齐后端 - **状态**:Done - 执行记录 1:`hooks/use-quote-polling.ts` deadline 驱动;`POLL_MAX_ATTEMPTS` 由常量推导 - 执行记录 2:`use-quote-polling.test.ts` 通过;`quote-result-panel` 文案 60s ### Step 10.21 page.goto 超时 → QUOTE_ENTRY_UNAVAILABLE - **根因**:`quote-page-adapter` goto 固定 20s;慢网/Cloudflare 下 `domcontentloaded` 未触发 - **修复**:`openQuoteEntryUrl` 45s + commit/load 回退;导航超时与错误文案增强 - **状态**:Done - 执行记录 1:`RPA_PAGE_GOTO_TIMEOUT_MS=45000`;`quote-page-adapter` 三档 waitUntil - 执行记录 2:`quote-error-messages` 提示 VPN/headless/storageState ### Step 10.20 询价 30s 超时:派送地址框定位失败 - **根因**:提货/派送共用 `搜索地址` selector;填完提货后仍命中提货框,Playwright 等 30s → `QUOTE_TIMEOUT` - **修复**:派送取 widget 内最后一个可见 textbox;双击「送货至」;page 默认超时 12s;候选按 display_label 去重 - **状态**:Done - 执行记录 1:`address-side.ts` delivery 用 `lastVisible` + 双点分区 - 执行记录 2:`session-manager` 降默认超时;`address-candidates` 去重 ### Step 10.19 移除货物类型表单项 + 联想键入对齐录制 - **需求**:隐藏「货物类型」;默认 `general_freight`;修复 Dallas 派送联想不出现 - **根因**:`formatAddressQuery` 未带邮编;`Garland` 误解析进 street - **状态**:Done - 执行记录 1:`quote-form.tsx` 移除货物类型;`ensureAddressSuggestions` 多策略键入 - 执行记录 2:`formatAddressQuery` 改为 `街道, 城市, 州 邮编`;`normalizeParsedStreet` 修正 ### Step 10.29 MotherShip 键入去掉邮编 + 表单移除邮编字段 - **需求**:RPA 联想键入不带邮编;查价表单取消邮编输入 - **修复**:`formatAddressQuery` 仅 `街道, 城市, 州, USA`;`quote-form` 移除邮编栏 - **状态**:Done - 执行记录 1:`address-suggestions.ts` 键入策略去 zip;候选 API zip 可空 - 执行记录 2:`quote-form.tsx` 移除邮编;zip 仍由 MotherShip 候选/联想结果补全 - **根因**:`mothership.ts` finally 调用 `shouldKeepBrowserOpen`/`captureRpaDebugScreenshot` 但 import 在重构中丢失 - **修复**:唯一实现迁至 `lib/rpa/env.ts`;`mothership.ts` 显式 import - **状态**:Done - 执行记录 1:`page-prep.ts` 改为 re-export;无局部 fallback - 执行记录 2:测试 import 路径对齐 ### Step 10.27 evaluate __name helper 污染 - **根因**:tsx/esbuild 向 `page.evaluate(fn)` 注入 `__name` 等 helper,browser context 未定义 - **修复**:`browser-eval-scripts.ts` 纯字符串脚本;全部 evaluate/waitForFunction 改传 string - **状态**:Done - 执行记录 1:7 处违规调用点已迁移;禁止 function 引用传入 evaluate - 执行记录 2:`browser-eval-scripts.test.ts` 无 __name 断言通过 ### Step 10.26 cargo hydration:mounted ≠ interactive - **根因**:React hydration 后 input 已 mounted 但 pointer-events/disabled 未解除;旧 ready 用 class+mounted 误判 - **修复**:`isCargoStateReady` 仅认 interactive 托盘输入;`waitForCargoInteractiveState` = waitForFunction - **状态**:Done - 执行记录 1:`CargoStateEvidence` 增 `blockedCargoFields` 对比 mounted vs interactive - 执行记录 2:`cargo-state.test.ts` 更新;禁止 class/mounted-only ready ### Step 10.25 evaluate boundary:probeCargoOnWidget scope leakage - **根因**:`widget.evaluate(() => probeCargoOnWidget(...))` 引用 Node 函数,浏览器 context 未定义 - **修复**:probe 逻辑完整内联至 evaluate 回调;`fillMountedCargoField` 已是自包含回调 - **状态**:Done - 执行记录 1:删除 Node 侧 `probeCargoOnWidget` 外部调用 - 执行记录 2:全 workers/rpa evaluate 审计无其他 Node 函数引用 ### Step 10.24 querySelector 空 selector crash - **根因**:`RPA_SELECTOR_QUOTE_WIDGET?.trim() ?? fallback` 对 env 空串 `""` 无效;未进 registry;evaluate 内 `document.querySelector("")` - **修复**:`getSelector()` fail-fast + 内置回退;`locator.evaluate(widgetEl)` 替代 evaluate 内拼 selector - **状态**:Done - 执行记录 1:`env.ts`/`selectors.ts`/`cargo-state.ts`/`page-prep.ts`/`address-side.ts`/`address-suggestions.ts` - 执行记录 2:`selectors.test.ts` 空串回退用例通过 ### Step 10.23 fillCargo cargo UI state machine - **根因**:失败截图 cargo 为折叠摘要态;地址填完后仍 delivery_active,未 settle → cargo_trigger 无效;旧逻辑用 placeholder visible 判 ready - **修复**:`cargo-state.ts` settle/trigger/waitCargoStateReady(active class + window + mounted enabled 组合) - **状态**:Done - 执行记录 1:禁止连点 nth(2);`fillMountedCargoField` 在 state ready 后执行 - 执行记录 2:`cargo-state.test.ts` + `mothership.cargo.test.ts` 通过 ### Step 10.22 fillCargo 根因:货运详情点击节点与录制不一致 - **根因**:录制 `page.locator('div').filter({hasText:'货运详情'}).nth(4)`;代码用 `widget>>filter>>nth=4` 或 `getByText().first()` → 点错节点,托盘 placeholder 永不出现 - **修复**:`clickRecordingCargoSection` + `page.getByPlaceholder` 主路径;以 placeholder 可见性验证展开成功 - **状态**:Done - 执行记录 1:删除 `ensureCargoSectionOpen`/widget 子选择器填表;对齐 115541/115059 - 执行记录 2:`mothership.cargo.test.ts` 通过;待 probe 至 scrapeQuotes ### Step 10.21 fillCargo 尺寸区展开 + 减少页面跳动 - **根因**:每字段重复 `scrollIntoView`/重开货运详情;托盘填完后尺寸 placeholder 未展开(录制需点展开钮) - **修复**:widget 滚动 4s 冷却;`ensureCargoDimensionsVisible`;货物区只开一次;`scrapeQuotes` 内自动点提交 - **状态**:Done - 执行记录 1:`page-prep.ts` 滚动冷却;`mothership.ts` fillCargo 重构 - 执行记录 2:单测通过;待 `probe:rpa` 验证至 scrapeQuotes ### Step 10.20 派送地址误命中已锁定提货框 - **根因**:`openAddressSide(delivery)` 跳过点「送货至」;`visibleStreetInput` 在仅 1 个 textbox 时误取只读提货框 - **修复**:派送强制双点分区;`deliverySectionOpened` 后才取最后一个可编辑框;`readonly/disabled` 校验 - **状态**:Done - 执行记录 1:`address-side.ts` 派送路径对齐录制 115059;`mothership.ts` 传 `deliverySectionOpened` - 执行记录 2:`address-side.test.ts` 通过;待 `probe:rpa` 验证 fillCargo ### Step 10.19 首页 Cookie + 滚动稳定 + headed 不闪退 - **根因**:CookieYes 遮挡 widget;报价区在首屏下方;resolveQuoteEntry 重复 preCheck 导致分区连点跳动;finally 立即关窗 - **修复**:`page-prep.ts` dismissCookie + scrollIntoView;resolveQuoteEntry 仅 stabilize;headed 默认 `shouldKeepBrowserOpen` - **状态**:Done - 执行记录 1:新增 `workers/rpa/page-prep.ts`;`address-side`/`quote-page-adapter`/`mothership`/`session-manager` 对齐 - 执行记录 2:`page-prep.test.ts` + `quote-page-adapter.test.ts` 通过;待 `probe:rpa` 实网验证 --- - **根因**:点选后用 `getByText` 复查 label;输入框回显仍含该文案 → 误判下拉未关闭 - **修复**:`isAddressSuggestionDropdownOpen` 检测 `.pac-container`/`listbox` 可见性 - **状态**:Done - 执行记录 1:移除 `fillAddressField` 点选后 getByText 复查逻辑 - 执行记录 2:未锁定改判下拉容器;失败时补按 Enter 重试 ### Step 10.17 RPA 填表地址点选:USA/粘连文案不匹配 - **根因**:用户确认 `display_label` 含 `, USA` 或空格格式;下拉 DOM 为 `StreetLos Angeles, CA`(录制 112133) - **修复**:`resolveDomSuggestionLabel` 重扫下拉模糊匹配;`labelToCandidate` 统一粘连 display_label - **状态**:Done - 执行记录 1:`fillAddressField` 先 wait 再 resolve 后点选 - 执行记录 2:`address-suggestions.test.ts` 粘连/USA 匹配用例通过 ### Step 10.16 询价失败展示可读错误原因 - **根因**:RPA 失败仅落库 `QUOTE_UNAVAILABLE`;前端写死通用文案 - **输出**:`quote-error-messages.ts`;`quote_record.error_message`;API `error_message` - **状态**:Done - 执行记录 1:`fallback-orchestrator` 保留 RPA 细分 error_code 并写入 error_message - 执行记录 2:单测 quote-error-messages/serializer/polling 通过 ### Step 10.15 MotherShip 联想不含邮编 - **规则**:RPA 仅键入/匹配街道+城市+州+国家;下拉项不展示邮编;zip 保留用户表单值 - **变更**:`formatAddressQuery` 去 zip;`labelToCandidate`/`applyMothershipCandidate` 补全 zip - **状态**:Done - 执行记录 1:`mothership.ts` 填表改用 `street, city, state, USA` 触发联想 - 执行记录 2:mock catalog 与单测对齐无邮编 display_label --- - **根因**:下拉项为 `1234 Warehouse StreetLos Angeles, CA`(无 zip);采集强制 `includes(90001)` + 仅扫 widget 内 DOM - **修复**:放宽门牌号/州/城市匹配;全页 `.pac-container` + `page.getByText` 对齐录制 - **状态**:Done - 执行记录 1:闪退为 `closeBrowser()` 正常行为,非 DOM 变化 - 执行记录 2:`address-suggestions.test.ts` 无 zip 下拉项解析通过 --- - **根因**:`.pac-item`/`[role=option]` 与 MotherShip widget 实际 DOM 不符;录制用 `getByText('1234 Warehouse StreetLos')` 点选 - **修复**:`workers/rpa/address-suggestions.ts` 增 zip/门牌号启发式采集 + widget getByText 回退 - **状态**:Done - 执行记录 1:`address-candidates.ts`/`mothership.ts` 共用联想等待与点选逻辑 - 执行记录 2:`.env` 增 widget listbox selector;`address-suggestions.test.ts` 粘连格式解析通过 --- ### Step 10.30 RPAContext contract validator - **根因**:`instanceof RPAContext` 失效时 `fromSession(ctx)` 把 Context 当 Page,致 `page.locator` undefined → `.filter` 崩溃 - **修复**:`context-validator` duck-typing + `resolveRPAContext`;steps fail-fast;数组 `ensureArray` - **状态**:Done - 执行记录 1:新增 runtime 别名、assertRPAContext、address-side/page-prep 改用 resolveRPAContext - 执行记录 2:kernel 单测通过;probe 验证 fillAddress 不再 contract 断裂 - **目标**:消除 mothership.ts patch 结构;RPAContext / SelectorGraph / StateEngine / ExecutionLayer - **输出**:`workers/rpa/kernel/`;mothership 薄编排;cargo-state/page-prep 委托 kernel - **状态**:Done - 执行记录 1:新增 context/selector-graph/state-engine/execution-layer/steps/orchestrator;禁止模块级 widgetScrollAt - 执行记录 2:RPA 相关单测 220+ 通过;fillCargo 状态流迁至 kernel/cargo-step ### Step 10.31 Network-First Quote Architecture - **目标**:报价仅来自网络层;禁止 DOM price extraction / tab scrape / embedded JSON fallback - **输出**:`quote-network-capture.ts`、`quote-schema-validator.ts`、`thin-browser-driver.ts`;删除 `dom-fallback.ts`/`client-state.ts` - **状态**:Done - 执行记录 1:pipeline 重写为 submit→network capture→strict validate;fail-fast `RPA_DATA_INVALID` - 执行记录 2:新增 `MOTHERSHIP_QUOTE_API_URL_PATTERNS` pinned endpoint + `RPA_QUOTE_NETWORK_SNIFF` 校准 ### Step 10.32 Contract-driven Quote Extraction - **目标**:报价匹配改为 response shape 契约;URL/pinned 仅 telemetry;Quote Contract Guard fail-fast - **输出**:`quote-contract-guard.ts`;`contractPayloads`;移除 URL runtime matching - **状态**:Done - 执行记录 1:契约必填 `rates.standard.lowest`、`rates.fastest|standard.fastest`、`rates.guaranteed` - 执行记录 2:禁止 partial decode fallback;`source=contract`;pinned 仅 `formatPinnedTelemetry` ### Step 10.33 首页精简 widget cargo 展开 - **根因**:`mothership.com/` 首页 widget 为「What are you shipping?」汇总,无独立 pallet 输入;`assertCargoReady` 仅认 pallet → `no_cargo_inputs` - **修复**:展开货物面板 + `weight|pallet` 就绪判定 + Cookie Accept 按钮 - **状态**:Done - 执行记录 1:`state-engine.triggerCargoSection` 增首页标签点击;`WAIT_CARGO_INTERACTIVE` 改用 cargoReadyRe - 执行记录 2:`isCargoStateReady` 单测;`selector-specs` 增 What are you shipping 回退 ### Step 10.34 cargo summary chip → drawer UI flow - **根因**:cargo UI 为 summary chip,须 click 才展开 drawer;input 探针在 chip 未展开时恒为空 - **输出**:`workers/rpa/cargo-editor.ts`;`fillCargoStep` 改为 expand → wait drawer → fill in drawer - **状态**:Done - 执行记录 1:新增 `expandCargoEditor`/`waitForCargoEditorOpen`;就绪条件改为 dialog/modal 可见 - 执行记录 2:不再调用 `triggerCargoSection`/`assertCargoReady`;state-engine/contract/network 未改 ### Step 10.35 cargo popover 检测修复 - **根因**:`cargo-editor.ts:19-26` `isCargoEditorOpen` 仅认 `role=dialog`;MotherShip 为无 dialog 的 popover(Weight/L/W/H) - **修复**:popover 以 ADD ITEM/Weight placeholder 为打开信号;chip 点击增 `getByRole(button,/Pallet/)` + force click - **状态**:Done - 执行记录 1:失败截图 `rpa-failure-2026-06-17T09-37-51-321Z.png` 证实 chip 已填地址后显示 1 Pallet - 执行记录 2:仅改 `workers/rpa/cargo-editor.ts`;日志 `[rpa] cargo-editor: popover opened` ### Step 10.36 中文精简 widget 跳过 cargo 填表 - **根因**:`cargo-editor.ts:185` 对无 Pallet chip 的中文 widget 仍强制 expand;DOM 诊断证实填址后 widget 无 cargo input(仅「货运详情」步骤文案) - **修复**:`isCargoUiPresent()` 为 false 时 skip expand/fill;英文 chip 流保留原逻辑 - **状态**:Done - 执行记录 1:`scripts/debug-cargo-chip.ts` 证实 headless 中文 UI 无 pallet/weight 节点 - 执行记录 2:probe 已通过 fillCargo;下一阻塞为 submit 后 `contract=0`(network 层,另案) ### Step 10.37 quote shape normalize - **根因**:`quote-contract-guard.ts` `findRatesObject` 仅浅层 `body.rates`;API 返回 `data.rates` 等变体 → `satisfiesQuoteContract` 恒 false → `contract=0` - **输出**:`quote-normalize.ts`(resolveRatesFromBody/normalizeQuote);契约仅校验 CanonicalQuote - **状态**:Done - 执行记录 1:`quote-step.ts` 导出 resolver;capture 用 `normalizeQuote` 过滤;`RPA_QUOTE_DEBUG_RAW` 失败日志 - 执行记录 2:单测覆盖 data.rates/result.rates/[0].rates ### Step 10.38 quote network visibility 诊断层 - **根因**:`json=0` 表示 submit 后无 JSON 候选进入 capture;需证明 A/B/C(XHR / navigation / 无请求) - **输出**:`quote-network-visibility.ts`;capture drain inflight;`scripts/debug-quote-network.ts`;失败时 `quote-debug` 打 trace summary - **状态**:Done - 执行记录 1:`RPA_QUOTE_NETWORK_VISIBILITY=true` 全量 REQ/RESP;post-submit url 日志;修复 async handler 竞态 - 执行记录 2:`.env.example` 增 visibility 开关;不改 kernel/contract/selector ### Step 10.39 Business Ready Gate(submit 前闸门) - **根因**:submit 点击无 quote 请求;Cookie 遮挡 + 表单未真正 ready 导致 inert click - **输出**:`quote-business-ready.ts`(waitForBusinessReadyState/debugBusinessStateSnapshot);`thin-browser-driver` 闸门拦截 - **状态**:Done - 执行记录 1:四门检测(button/cookie/cargo lock/hydration+DOM stable)+ pointer hit-test - 执行记录 2:单测 `quote-business-ready.test.ts`;未改 kernel/contract/network capture ### Step 10.40 cargo 锁定(英文入口 + 强制填表) - **根因**:中文精简 widget 无 cargo editor → skip 填表 → business-ready 货物未锁定 - **修复**:`RPA_BROWSER_LOCALE=en-US` + `get-a-quote` URL;`fillCargoStep` 禁止 skip;`cargo-lock.ts` 关闭 drawer 并校验 pallet 摘要 - **状态**:Done - 执行记录 1:`session-manager` context locale/Accept-Language;`expandCargoEditor` 无 UI 时 fail-fast - 执行记录 2:`.env` 更新 URL/locale/submit selector;`cargo-lock.test.ts` 通过 ### Step 10.41 中文内联货物步填表与锁定 - **根因**:`货运详情` 展开为 widget 内联 input 非 popover;`locator(placeholder=)` 无效;摘要为 `2 托盘s` - **修复**:`isCargoFieldsVisible`/`resolveSpecOnScope(getByPlaceholder)`;`settleAddressFieldsForCargo`;摘要 regex 含托盘s - **状态**:Done - 执行记录 1:`expandCargoEditor` 点击货运详情步骤标签;`fillInCargoEditor` 修正 placeholder 定位 - 执行记录 2:`isCargoSummaryLocked` 认摘要文案+inputValue;business-ready 同步托盘s 模式 ### Step 10.42 Cargo Commit Barrier - **根因**:summary chip 可见但 placeholder 仍可编辑 → 假锁定 → submit 无 quote flow - **输出**:`waitForCargoCommittedState`/`assertCargoCommittedState`;错误码 `CARGO_NOT_COMMITTED` - **状态**:Done - 执行记录 1:committed 需 summary + 无 editable placeholder + internal pallet/weight 值 - 执行记录 2:`fillCargoStep`/`submitQuoteForm`/`business-ready` 统一走 commit barrier ### Step 10.43 重量提交与 committed 判定修复 - **根因**:重量未 commit → 摘要 `-Lbs`;`226Lbs` 无空格未匹配;wait 轮询 `attemptCargoCommit` 重置向导 - **修复**:重量单独填+Tab commit;摘要完整即 committed;移除 wait 轮询 commit;widget-only 填表 - **状态**:Done - 执行记录 1:`evaluateCargoCommitted` 识别 `226Lbs`;拒绝 `-Lbs`;内联步摘要完整可放行 - 执行记录 2:`debug-post-submit` submit 跳转 `sign-up-quote?quote-details=...` ### Step 10.44 货物 UI 等待与 business-ready 地址闸门 - **根因**:地址填完后货物步渲染延迟 → `isCargoUiPresent` 立即 fail;`probeWidgetDom` 地址判定与内联货物步不一致 - **修复**:`waitForCargoUiPresent` + `triggerCargoSection`;`isCargoWizardPastAddress` 统一地址锁定判定 - **状态**:Done - 执行记录 1:`fillCargoStep` 轮询等待货物 UI,去除立即 `QUOTE_ENTRY_UNAVAILABLE` - 执行记录 2:`quote-business-ready` 用 `isCargoWizardPastAddress` 替代 DOM 启发式 ### Step 10.46 双地址强闸门与 submit 跳转校验 - **根因**:`isAddressesLockedForSubmit` 单州码弱放行 → submit 无跳转 → `contract=0 json=0` - **修复**:双州码/双邮编/双街道强判定;`fillAddressStep`/`submitQuoteForm` 双地址 assert;submit 未跳转 fail-fast;等待 `axel/quote` - **状态**:Done - 执行记录 1:`evaluateAddressesLockedForSubmit` 移除 `stateCount>=1`;business-ready 去掉 cargo 绕过地址 - 执行记录 2:`submitQuoteForm` 未跳转抛错;`quote-network-capture` 等待 axel/quote 响应 ### Step 10.47 Worker Redis 稳定性与全栈验收 - **根因**:WSL 休眠致 Redis 间歇断开;`maxRetriesPerRequest:3` 致心跳抛错;此前仅 probe 不构成 worker 验收 - **修复**:`family:4` + `maxRetriesPerRequest:null`;worker 自动拉起 WSL infra + Redis 恢复;`verify:rpa-stack` 四步验收;WSL keepalive - **状态**:Done - 执行记录 1:加强 `verify:rpa-stack` worker 步:30s 观察 + 首次心跳后再稳 15s + 检测 MaxRetries/心跳失败 - 执行记录 2:`npm run verify:rpa-stack` 四步全绿;`start-wsl-infra` 增加 WSL keepalive 防休眠 ### Step 10.48 Redis 主机解析与验收环境同步 - **根因**:WSL localhost 转发间歇失效;`verify:rpa-stack` 未加载 `.dev/infra.env`;`Resolve-InfraHost` 仅 TCP 探测优先 127.0.0.1 - **修复**:优先 WSL IP + Node 新建连接 PING;`loadDevEnv` 双文件加载;spawn 合并 infra.env;worker 前摇探针 + BullMQ error handler - **状态**:Done - 执行记录 1:`lib/dev-env.ts` + `Resolve-InfraHost` 改用 `Test-RedisPingFromNode`;`infra.env` 写入 `WSL_HOST_IP` - 执行记录 2:`verify:rpa-stack` spawn 前 `probeFreshRedisConnection`;`ensureRedisReady` 30 次重试 + WSL 唤醒 ### Step 10.49 反爬伪装与 sign-up-quote 落点修复 - **根因**:SPA 跳转不触发 `waitForURL` 误报未跳转;Playwright Chromium 自动化指纹;缺少首页预热 - **修复**:`post-submit` URL 轮询;`playwright-extra` stealth + `RPA_BROWSER_CHANNEL`;`RPA_HUMAN_ENTRY_FLOW`;`RPA_NETWORK_DEBUG` - **状态**:Done - 执行记录 1:`lib/rpa/browser-launch.ts` stealth/patchright/chrome;`thin-browser-driver` 接受 sign-up-quote - 执行记录 2:`quote-page-adapter` 首页预热;sign-up-quote 页延长 network settle;单测 post-submit-navigation ### Step 11.14 MotherShip listbox 多候选采集修复 - **根因**:`enumerateAddressCandidates` 先用含 USA 的完整 query 仅得 1 项;`labelToCandidate` L355-371 解析失败回退用户输入生成 DrDallas/BlvdLos;listbox 双行未转 `主|副`;`parsePacStructuredLabel` 误把 USA 当州码 - **修复**:`buildAddressEnumerationQueries` 粘连 query 优先;`COLLECT_WIDGET_SUGGESTION_LABELS`;删除 query 回退;`option_id` 去重 - **状态**:Done - 执行记录 1:`browser-scripts.ts`/`address-suggestions.ts` widget 采集与枚举链路 - 执行记录 2:`address-candidates.ts` option_id 去重;单测 Garland/TX/粘连 query ### Step 11.13 Redis WSL IP 失效回退 127.0.0.1 - **根因**:`.dev/infra.env` 写入过期 WSL IP `172.26.233.197`,覆盖 `.env` 的 `127.0.0.1`;WSL 休眠后 Connection is closed - **修复**:`Resolve-InfraHost` 优先 127.0.0.1;`buildRedisCandidateUrls` + `ensureRedisReady`/`withBoundedRedis` 自动回退 - **状态**:Done - 执行记录 1:`lib/redis.ts` 多 URL 探针与 adoptRedisUrl - 执行记录 2:`ensure-infra.ps1` 解析顺序调整;`.dev/infra.env` 改回 127.0.0.1 ### Step 11.12 pac-item 多候选 suburb 解析修复 - **根因**:innerText 粘连 + `labelToCandidate` 将 Garland/Wilmer 均解析为 query.city(Dallas),`display_label` 去重后只剩 1 项 - **修复**:`COLLECT_PAC_ITEM_LABELS` 主行|副行采集;`parsePacStructuredLabel` 以 suburb 为 city;去重改 raw label - **状态**:Done - 执行记录 1:`browser-scripts.ts`/`address-suggestions.ts` 结构化 pac 采集与解析 - 执行记录 2:`address-candidates.ts` 去重键;单测 Garland/Wilmer/Farmers Branch ### Step 11.11 地址候选 180s 边界超时修复 - **根因**:`enumerateAddressCandidates` 全量 query 各 12s × 双侧超 180s,前端 Abort;Redis 断连时轮询读不到 worker 结果 - **修复**:主 query 12s + 最多 2 个短 query 5s,≥2 项即停;超时链提至 210s;Redis 探针与不可用快失败 - **状态**:Done - 执行记录 1:`address-suggestions.ts` 智能枚举;`constants`/`route` 210s/300s - 执行记录 2:`candidates-queue.ts` ensureRedisReady + redis_unavailable 区分 ### Step 11.10 多候选枚举与报价超时对齐 - **根因**:联想首条即返回漏采其余 pac-item;前端/后端报价超时 60s,RPA 候选+询价合计超 60s - **修复**:`enumerateAddressCandidates` 多 query 合并 + 稳定等待;`QUOTE_TIMEOUT_MS`/`POLL_TIMEOUT_MS`/`RPA_JOB_TIMEOUT_MS` 提至 150s/150s/120s - **状态**:Done - 执行记录 1:`address-suggestions.ts` 稳定采集与 pac-item 选择器优化 - 执行记录 2:`address-candidates.ts` 改 `enumerateAddressCandidates`;超时链与单测同步 ### Step 11.9 地址确认弹窗强制展示与展示去邮编 - **根因**:`requires_selection=false` 时前端跳过确认弹窗自动询价;`CandidateGroup` 单候选不渲染;本地 mock `formatted_address` 含邮编 - **修复**:RPA 返回后一律弹出确认窗;单候选也展示待确认项;`formatAddressDisplayLabel` 展示不含邮编 - **状态**:Done - 执行记录 1:`embedded-quote-widget.tsx` 移除自动择一;`address-disambiguation-modal.tsx` 单候选可见 - 执行记录 2:`format-address.ts` + `quote-form.tsx` 已选择文案去邮编;mock catalog 同步 ### Step 11.8 地址候选与询价统一 RPA Worker 链路 - **根因**:子进程 spawn 与 worker 双浏览器争用 `.rpa/mothership-storage.json`;spawn 路径可联想不可报价,worker 路径可报价不可联想 - **修复**:候选 job 入 BullMQ 同一 worker;`withRpaSessionLock` 串行;`address-candidates` 复用 `openQuotePage`/`address-side`/`address-suggestions`;API 轮询 Redis 结果 - **状态**:Done - 执行记录 1:`candidates-queue.ts`/`candidates-job-handler.ts`/`queue.ts`/`index.ts` 接线;`mothership-candidates.ts` 改 worker 队列 - 执行记录 2:`address-candidates.ts` 对齐 quote 会话链路;移除 `maxQueries:2`;单测 mock worker 队列 ### Step 11.4 dev:start prisma generate EPERM 修复 - **根因**:旧 next/worker 进程占用 `query_engine-windows.dll.node`;Stop 后仅等 500ms,Windows 未及时释放 DLL - **修复**:`Wait-DevProjectProcessesGone` + `Wait-PrismaEngineWritable` + `Invoke-PrismaGenerateSafe` 重试 - **状态**:Done - 执行记录 1:`lib.ps1` 增强进程识别/kill 与 engine 可写探测;`start.ps1` 改用安全 generate - 执行记录 2:失败时清理 `.tmp` 引擎文件并提示 `npm run dev:stop` ### Step 11.7 地址联想前端 Abort 超时与派送框定位 - **根因**:RPA ~119s 与前端 Abort 120s 竞态;`getVisibleStreetInput` 未传 `deliverySectionOpened` 派送侧反复失败 - **修复**:超时链 180s/150s;派送框定位;候选 `maxQueries:2`;限流 bounded Redis - **状态**:Done - 执行记录 1:`address-side.ts`/`constants`/`route.ts` 超时与 RPA 优化 - 执行记录 2:`rate-limiter.ts`/`ip-rate-limiter.ts` 使用 `withBoundedRedis` ### Step 11.6 地址候选 API Playwright 子进程隔离 - **根因**:Next API 直接 import Playwright → `ERR_INVALID_FILE_URL_PATH` 500 → 前端「响应解析失败」 - **修复**:`spawnFetchMothershipAddressCandidates` 子进程跑 `scripts/rpa/fetch-address-candidates.ts` - **状态**:Done - 执行记录 1:`mothership-candidates.ts` 改 spawn;新增 `lib/rpa/spawn-address-candidates.ts` - 执行记录 2:`api-client.ts` 非 JSON 友好提示;route 捕获非 RpaError 返回 503 JSON ### Step 11.5 dev:start Prisma stderr 误杀脚本 - **根因**:`$ErrorActionPreference=Stop` 下 node stderr 警告被 PowerShell 当 NativeCommandError 终止;非 EPERM - **修复**:`Invoke-DevNativeCommand`/`Invoke-NpmScript`;新增 `prisma.config.ts` 消除 package.json#prisma 警告 - **状态**:Done - 执行记录 1:`lib.ps1`/`start.ps1` 外部命令统一 Continue 捕获 exit code - 执行记录 2:`package.json` 移除 prisma 块;seed 迁入 `prisma.config.ts` ### Step 11.3 MotherShip 地址联想超时修复 - **根因**:Headless 下 quoter widget 骨架屏未 hydration 即填表;Google Places 响应未等待;5s 联想超时过短;Redis 单例无限重试拖慢候选 API - **修复**:`waitForQuoterWidgetHydrated` + `preCheck`;Places 网络/pac-item 等待;候选联想 12s;bounded Redis 写入 - **状态**:Done - 执行记录 1:`page-prep.ts`/`address-side.ts`/`address-suggestions.ts`/`address-candidates.ts`/`quote-session-store.ts` - 执行记录 2:`page-prep.test.ts` hydration 探针用例;待实网验证候选 API 多选弹窗 ### Step 11.18 返回 MotherShip 全部报价档位 - **根因**:`payload-decode`/`quote-schema-validator` 强制归一化为 4 档,`bestValue` 被丢弃或合并 - **修复**:扩展 `rateOption=bestValue`;归一化保留核心 4 档 + 可选 bestValue;前端动态展示 - **状态**:Done - 执行记录 1:`quote-completeness`/`quote-schema-validator`/`payload-decode` 支持 4~6 档 - 执行记录 2:`quote-card` 动态 rate tab;单测 axel fixture 含 standard.bestValue ### Step 11.19 「已选择」地址展示逻辑修复 - **根因**:`quote-form` 在本地 mock 联想点选后即显示「已选择」,未等 MotherShip 弹窗确认 - **修复**:仅 `selected_from_mothership=true` 时展示;弹窗确认后由 widget 回传 confirmed 地址 - **状态**:Done - 执行记录 1:`AddressSection` 改读 `confirmed` prop,不再用本地 `selected` 展示 - 执行记录 2:`embedded-quote-widget` 确认后 setConfirmedPickup/Delivery,改地址草稿时清除 - **根因**:UI 走 `requestMothershipCandidatesViaWorker` BullMQ 轮询;WSL Redis 间歇断开时 job 入队但 worker 无法消费/写回结果,API 空轮询 210s;`probe:rpa` 直连 RPA 不经队列故正常 - **修复**:`isAnyRpaWorkerHealthy` + 队列 waiting 25s stall 检测;不可用时回退 `spawnFetchMothershipAddressCandidates` - **状态**:Done - 执行记录 1:CLI 实网 pickup 4 / delivery 2 约 112s;verify-address-candidates 142s 成功(spawn 回退) - 执行记录 2:`candidates-queue.test.ts` + `worker-health.test.ts` 4/4 通过 --- ### Step 11.16 街道-only 联想 label 解析失败 - **根因**:`listSideCandidates` L91-96;MotherShip 返回 `1234 Warehouse Street` 等仅街道行,`labelToCandidate` L377 返回 null - **修复**:`parseStreetOnlySuggestion` 补全 city/state;pipe 行 `isMotherShipEnumerationRow` 不再误杀 streetToken - **状态**:Done - 执行记录 1:`address-suggestions.ts` 新增 street-only 解析;pipe 副行 echo 判定 - 执行记录 2:CLI 实网 pickup 3 项 / delivery 2 项;单测 16/16 ### Step 11.15 地址枚举 ArrowDown 与 echo 过滤修复 - **根因**:枚举路径 `ArrowDown` 收起 listbox 仅留 1 项;`isMotherShipEchoCandidate` 仅比 display_label 把不同街道/城市候选误杀 - **修复**:`forEnumeration` 跳过 ArrowDown;listbox 可见等待;echo 需 street+city 均匹配;pipe 标签二次吸收 - **状态**:Done - 执行记录 1:`address-suggestions.ts` 枚举专用 trigger + echo 判定放宽 - 执行记录 2:`address-candidates.ts` 空候选时 pipe 标签跳过 echo;单测 14/14 ### Step 11.0 阶段0 Network-First 录证脚本 - **输入**:方案 A 批准;GD-15 匿名入口 URL - **输出**:`scripts/phase0/record-baseline.ts` + `verify-baseline.ts` + `docs/phase0-recording-checklist.md` - **产物**:`.rpa/phase0/baseline.har`、`quote-contract.schema.json`、`session-storage-snapshot.json`(人工执行后生成) - **约束**:独立脚本,不修改 workers/rpa 任何代码 - **状态**:Done(脚本就绪;verify:phase0 7/7 PASS) - 执行记录 1:headed 双检查点录证(地址锁定→存储快照→提交报价→HAR+Schema) - 执行记录 4:A3 误报修复;axel/quote 契约为 data.rates + availableRates ### Step 11.2 阶段1 单会话 QuoteSession - **输入**:Step 11.1 axel 契约;阶段0 session-storage-snapshot - **变更**:`quote-session-store` Redis+文件;候选 API 返回 `quote_session_id`;询价复用 storageState - **验证**:quote-session-store 单测;候选→询价 session 透传 - **状态**:Done - 执行记录 1:`address-candidates` 保存 session + persistContext;`openQuotePage(sessionId)` 加载 - 执行记录 2:前端/API `quote_session_id` 透传;`quote-mapper`/`mothership` 复用会话 ### Step 11.1 阶段1 axel/quote Contract-Driven Capture - **输入**:quote-contract-summary.json;阶段0 录证 7/7 - **变更**:`axel-contract.ts`;guard 接受 guaranteed.bestValue;decode 映射 bestValue/days;axel URL 优先 - **验证**:`quote-capture.test.ts` axel fixture 四档解析通过 - **状态**:Done - 执行记录 1:`quote-contract-guard` lowest/bestValue + baseQuotePrice/finalPrice 价格键 - 执行记录 2:`payload-decode` guaranteed.bestValue→lowest;axel URL 排序优先;单测通过 ### Step 10.50 sign-up-quote 匿名流与 Chrome channel - **根因**:匿名成功落点为 `sign-up-quote`(非登录墙);Playwright HeadlessChrome 指纹致 `axel/quote` 不返回;`waitForURL` SPA 误报 - **修复**:URL 轮询接受 sign-up-quote;`RPA_BROWSER_CHANNEL=chrome` + sign-up 页 18s widget 等待;捕获 `services.mothership.com/axel/quote` - **状态**:Done - 执行记录 1:`RPA_USE_STEALTH=false`(stealth 破坏 widget);`probe:rpa` 在 chrome channel 下 4 档全绿 - 执行记录 2:网络日志确认 HeadlessChrome 上报;sign-up-quote 后 widget 触发 axel/quote ### Step 11.19 报价解析精度与地址枚举 query 修复 - **根因 A**:`payload-decode` `walkJson` 递归误采嵌套对象;`readPrice` 未优先 `finalPrice`;`readTransit` 将 `days:0` 格式化为 0 business days;`normalizeQuoteItems`/`normalizeToFourTiers` 复制 lowest 伪造 fastest - **根因 B**:`buildAddressEnumerationQueries` 粘连 query 首条且无 USA;枚举次 query 仅 5s 超时 - **修复**:结构化解析 `rates.{standard,guaranteed}.{lowest,fastest,bestValue}`;只返回 API 真实档位;枚举首条 `formatAddressQuery`;统一 12s 超时 - **状态**:Done - 执行记录 1:`payload-decode.ts`/`quote-schema-validator.ts`/`quote-completeness.ts` 最小改动 - 执行记录 2:`address-suggestions.ts` 枚举 query 顺序与超时对齐 autocomplete;单测更新通过 ### Step 11.20 邮编可选 + 准确度测试从组 01 重跑 - **变更**:询价 API `zip` 改为可选(空字符串通过);测试脚本 10 组地址不再填邮编 - **状态**:Done - 执行记录 1:`modules/quote/validation.ts` zip 校验与 address lookup 对齐 - 执行记录 2:`generate-accuracy-test-data.ts` 去掉 zip 字段,清空进度从组 01 重跑 ### Step 11.21 报价 UI 档位对齐 + 地址枚举 filter 修复 - **根因 A**:`payload-decode.ts` 提取 `guaranteed` 档且 `readPrice` 未锁定 `finalPrice`;`normalizeQuoteItems` 输出含 guaranteed 四档 - **根因 B**:`collectEnumerationLabels`(592 行)`isMotherShipEnumerationRow` 过严,真实 pac-item 被滤空;`ENUMERATE_MAX_QUERIES=3` 截断 query - **修复**:仅输出 `UI_QUOTE_TIERS`(standard×3);`finalPrice` 优先;枚举 query 与 autocomplete 统一;filter 空时回退 merged - **状态**:Done - 执行记录 1:`payload-decode.ts`/`quote-schema-validator.ts`/`quote-completeness.ts`/`lib/constants/quote.ts` - 执行记录 2:`address-suggestions.ts` 枚举 filter 回退 + query 统一;单测 39/39 通过 ### Step 11.22 准确度测试对齐用户 HTTP 查价路径 - **变更**:`generate-accuracy-test-data.ts` 弃用 Worker 直连/Redis 轮询,改用 `hostFetchMothershipCandidates` + `hostCreateQuote` + `pollQuoteUntilDone`(与 EmbeddedQuoteWidget 一致) - **状态**:Done - 执行记录 1:候选/询价/轮询均走 Next HTTP API + x-customer-id 头 - 执行记录 2:确认候选用 `applyMothershipCandidate`;启动前检查 Next 就绪 ### Step 11.23 地址 WhenStable 采集 + finalPrice 防污染 + 超时 300s - **根因 A**:`enumerateAddressCandidates` 801 行 `waitForAddressSuggestions` 误报 false 时 `continue` 跳过采集(Headless 下 DOM 已有联想) - **根因 B**:`resolveTierObject` 356 行用 `availableRates` extended/customRate 覆盖 `data.rates.standard.*.finalPrice` - **根因 C**:`CANDIDATES_FETCH_TIMEOUT_MS`/`ADDRESS_CANDIDATES_TIMEOUT_MS`=210s < 实际 RPA ~285s - **修复**:改用 `collectAddressSuggestionLabelsWhenStable`;叶节点有 finalPrice 则不合并 extended;超时 300s - **状态**:Done - 执行记录 1:`address-suggestions.ts` 枚举去掉 waitFor 门控;`payload-decode.ts` resolveTierObject 防污染 - 执行记录 2:超时常量 300s;单测覆盖 $287/$345/$457 finalPrice 场景 ### Step 11.20 RPA 询价链路性能优化(P0~P4) - **目标**:缩短候选→询价端到端耗时,不影响 RPA 功能与四档验收 - **变更**:Browser 保活;quote_session 复用跳过预热/重复填址;收紧 sign-up settle;自适应轮询 - **验证**:address-step/env/quote-page-adapter/use-quote-polling 单测通过 - **状态**:Done - 执行记录 1:`shouldKeepBrowserWarm` 默认 headless 保活;`address-candidates` 不再每次 `closeBrowser`;`fillAddressStep` session fast-path - 执行记录 2:`RPA_SESSION_WIDGET_TIMEOUT_MS=15s`;`RPA_SIGN_UP_QUOTE_SETTLE_MS=10s`;轮询 10s 后改 800ms ### Step 11.21 候选+询价单次 RPA 会话(驻留页 + 并行预热) - **目标**:候选完成后驻留 page/context;用户确认弹窗期间预热;询价同页续跑不重开 - **变更**:`parked-quote-session` 池;候选 park;询价 take;preheat/release API + 前端弹窗联动 - **验证**:parked-quote-session 单测;驻留不可用回退 storageState 开页 - **状态**:Done - 执行记录 1:`address-candidates` park 后不关页;`mothership` 优先 `takeParkedQuoteSession` - 执行记录 2:preheat/release BullMQ job;embed 弹窗打开预热、取消释放 ### Step 11.24 队列卡死 + 询价超时 + preheat jobId - **根因 A**:Redis 队列存在 orphan `active` address-candidates job,阻塞 Worker;`QUEUE_STALL_MS=25s` 在 Worker 忙时 spawn 第二套 Playwright - **根因 B**:`QUOTE_TIMEOUT_MS`/`RPA_JOB_TIMEOUT_MS`=150s/120s < 实测 RPA ~285s → 测试/前端先报 timeout、DB 孤儿 processing - **根因 C**:`addParkedSessionJob` jobId 含 `:` → BullMQ 抛 `Custom Id cannot contain :`,preheat 500 - **修复**:stall 改 60s 且 active>0 不 spawn;超时 300s;jobId 改 `_`;Worker stalledInterval - **状态**:Done - 执行记录 1:`candidates-queue.ts`/`queue.ts`/`quote.ts`/`rpa.ts`/`workers/rpa/index.ts` - 执行记录 2:清 orphan active job;单测更新;待重启 worker+next 后仅跑组 01 验证 ### Step 11.26 动态 UI 档位:MotherShip 返回多少输出多少 - **需求**:不同地址可能是 2+1 / 2+2 / 3+3 等,禁止固定 3+2 白名单过滤 - **修复**:`extractTiersFromServiceBlock` 动态遍历 rates 子键;`normalizeQuoteItems`/`normalizeToFourTiers` 全量去重排序 - **状态**:Done - 执行记录 1:`payload-decode.ts`/`quote-schema-validator.ts`/`quote-completeness.ts`/`quote.ts` - 执行记录 2:单测覆盖 2+1/2+2/3+3/5 档;仍只读 rates 叶节点 finalPrice ### Step 11.27 E2E 运行环境:去 spawn 双开 + orphan 回收 + 360s 超时 - **根因**:Worker 忙时 stall spawn 双开 Playwright;orphan active job 阻塞队列;300s 轮询不足 - **修复**:仅 Worker 不可用时 spawn;`recoverOrphanActiveJobs` 启动回收;并发 1;超时 360s;测试脚本全栈就绪检查 - **状态**:Done - 执行记录 1:`candidates-queue.ts`/`queue-recovery.ts`/`index.ts`/`generate-accuracy-test-data.ts` - 执行记录 2:`QUOTE_TIMEOUT_MS`/`RPA_JOB_TIMEOUT_MS`=360s;`pollQuoteUntilDone` 支持自定义 timeout ### Step 11.28 sweeper 宽限期:420s 超时避免误杀 active RPA job - **根因**:`QUOTE_TIMEOUT_MS` 与 `RPA_JOB_TIMEOUT_MS` 同为 360s,sweeper 在 RPA 完成前 `moveToFailed` active job - **修复**:`QUOTE_TIMEOUT_MS`=420s(RPA 360s + 60s grace);lock +15s;测试脚本启动时 orphan 回收 - **状态**:Done - 执行记录 1:`lib/constants/quote.ts`/`queue.ts`/`generate-accuracy-test-data.ts` - 执行记录 2:单测 timeout 阈值 421s;轮询默认跟随 `QUOTE_TIMEOUT_MS` ### Step 11.29 WSL keepalive:长跑 RPA 期间 Redis/MySQL 不断连 - **根因**:`dev:start` 未启 `wsl-keepalive`,WSL sleep 导致 6379/3307 ECONNREFUSED,队列 job 无法消费 - **修复**:`Ensure-DevInfra`/`Start-WslInfra` 启动 keepalive;测试脚本每案前 `ensureInfraStableForCase` - **状态**:Done - 执行记录 1:`scripts/dev/ensure-infra.ps1` 两处 `Start-WslKeepAlive` - 执行记录 2:`generate-accuracy-test-data.ts` 双次就绪探测 + 案前 orphan 回收 ### Step 11.25 验收对齐截图:UI 五档 + finalPrice + 基线地址 - **验收**:标准 Tab 3 档($287.23/$345/$369.72)+ 保证送达 2 档($457.36/$480.37) - **修复**:`UI_QUOTE_TIERS` 扩至 5;`extractTiersFromRates` 解析 guaranteed;`resolveUiTierLeaf` 禁止 availableRates 合并 - **状态**:Done - 执行记录 1:`payload-decode.ts`/`quote.ts`/`quote-schema-validator.ts`/`quote-completeness.ts` - 执行记录 2:组 01 地址改为 Warehouse Street + Distribution Way Farmers Branch TX;单测五档验收 - **原则**:不缩短第三方 API 等待;填表保留 input/change + fill 校验回退;填表后 100~300ms 随机延迟 - **变更**:`RPA_SIGN_UP_QUOTE_SETTLE_MS` 恢复 18s;新增 `lib/rpa/human-pacing.ts`;地址/货物填表后 `awaitHumanPacing` - **验证**:integration 42/42;RPA 相关单测 30/30 - **状态**:Done - 执行记录 1:耗时瓶颈在 page.goto/widget hydration 与 axel/quote 网络响应,非本地 fill - 执行记录 2:address-suggestions 已有 dispatchEvent+sequential 回退;cargo-editor fill 后加人类节奏 ### Step 11.30 单次 RPA 会话 confirm 闭环 + RPA_WORKER_ID 修复 - **根因 A**:`.env` 中 `RPA_WORKER_ID` 为空 → `canPersistParkedQuoteSession()` 恒 false → 候选从不 park - **根因 B**:前端确认后直接 `submitQuote`,未在同页锁定双地址 → 询价仍完整 `fillAddress` + `openQuotePage` - **根因 C**:spawn 子进程候选时 park 在子进程内存,退出即失效 - **修复**:worker 启动写入 `process.env.RPA_WORKER_ID`;`hostConfirmMothershipAddresses` + embed confirm API;`RPA_PARKED_SESSION≠false` 禁止 spawn - **状态**:Done - 执行记录 1:`workers/rpa/index.ts` 自动设置 RPA_WORKER_ID;`embedded-quote-widget` 确认后调 confirm API - 执行记录 2:`api-client` 恢复 preheat + 新增 confirm;测试脚本与 candidates-queue 单测对齐 ### Step 11.31 放弃驻留页默认路径,改稳定双阶段 + storageState - **决策**:驻留页需 worker+confirm+同进程,验证周期 >5min 且 fetch failed 频发;MotherShip 网络等待占 80%+,驻留页收益有限 - **变更**:`RPA_PARKED_SESSION` 默认关(仅 `=true` 时启用);去掉前端 confirm 往返;候选只产 storageState,询价 job 一次开页填址+提交 - **状态**:Done - 执行记录 1:`isParkedSessionEnabled()` 统一开关;embed/测试脚本不再调 confirm API - 执行记录 2:保留 browser warm + reuseSession preCheck 快路径;spawn 回退恢复可用 ### Step 11.33 货物单托重量语义 + session 残留覆写 - **根因**:`quote_session` storageState 残留 widget 货物摘要(如 weightLbs=2296);commit 闸门只验托盘数不验单托重量 - **修复**:`cargo-weight.ts` 单托语义校验;`fillCargoStep` 检测残留 → 关闭 editor → forceClear 覆写 → commit 后断言重量 - **状态**:Done - 执行记录 1:新增 `probeWeightMatchesExpected` / `isStaleCargoState`;`waitForCargoCommittedState` 支持 `ExpectedCargoCommit.weight` - 执行记录 2:`fillInCargoEditor` 增加 `forceClear`;`cargo-step` 失败重填单托重量;单测 `cargo-weight.test.ts` ### Step 11.38 fillCargoInDrawer 动态字段 + 浏览器防翻译 - **变更**:`fill-cargo-in-drawer.ts` 废弃静态 WEIGHT/LENGTH 选择器;browser TranslateUI 禁用 + locale en-US + notranslate - **状态**:Done - 执行记录 1:`fillCargoInDrawer` 动态 placeholder 填托数/重量/尺寸 + Tab + 摘要断言 - 执行记录 2:`cargo-step` 仅调用 fillCargoInDrawer;`RPA_CARGO_DRAWER_PAUSE` 控制 page.pause ### Step 11.37 重量单位延迟至填表前解析 + 字段等待 - **根因**:`buildExpectedCargo` 在重量字段未渲染时默认 lb,内联 kg 字段填 lb 值失败 - **修复**:`fillNewFreightLine` 填完尺寸后再 `resolveMotherShipWeightDisplayFromPage`;`waitForFieldLocator` 12s - **状态**:Done - 执行记录 1:`cargo-step` 重量解析移至填表前;全字段 `commit:true` 推进向导 - 执行记录 2:`fillInCargoEditor` 默认 commit false;`valuesEquivalent` 数值等价校验 ### Step 11.36 重量单位按页面字段 + 填值生效 + 地址锁定 - **根因**:env 多 spec 合并含 "kg" 误换算 503→228.16;fill 无 value 校验;quote_session 跳过填址致 widget 回退 - **修复**:`readLiveWeightFieldMeta` 读激活字段单位;`fillLocatorWithVerify` + editor 断言;删 quote_session 跳过、强制点选候选 - **状态**:Done - 执行记录 1:`cargo-weight.ts` / `cargo-field-fill.ts` 按页面 lb/kg 填原始值;`cargo-step` commit 前 `waitForCargoEditorFieldValues` - 执行记录 2:`address-step` 始终 fillAddress + `widgetContainsConfirmedAddress`;单测 cargo-weight / address-step / cargo-step 更新 ### Step 11.35 MotherShip 货物彻底重置 + 询价禁用 storageState - **根因**:quote_session storageState / localStorage 残留货物;forceClear 无法删旧 freight 行 - **修复**:`cargo-reset.ts` 删行→ADD ITEM→填表→`assertWidgetCargoSummary`;询价 `freshCargoContext` 不复用 storageState - **状态**:Done - 执行记录 1:`removeAllFreightRows` / `clickAddFreightItem`;`openQuotePage({ freshCargoContext: true })` 清 storage - 执行记录 2:`mothership.ts` 询价跳过 persistContext;摘要重量支持单托/总重双匹配 - **变更**:`prove-rpa-chain.ts` 支持 `PROOF_PAIR_ID=01|02|06` 切换地址与货物参数 - **状态**:Done - 执行记录 1:新增 PROOF_PAIRS 映射;报告 JSON 含 proofPairId/label - 执行记录 2:组 06 两次跑:QTE_0025 超时(旧 worker);QTE_0026 货物重量闸门拦截 457 vs 2079Lbs ### Step 11.39 AxelSelectionBridge:search → placeId → place/{id} commit - **根因**:`address-step.ts` `selectAddressSuggestion` 仅 DOM label 点选,input.value 更新但无 `placeId`,不触发 `GET place/{id}` - **修复**:新增 `axel-selection-bridge.ts` 监听 search 响应绑定 placeId;`selectAddressWithPlaceBinding` 按 description 点选并等待 place 200;commit 以网络证据为准 - **状态**:Done - 执行记录 1:`axel-selection-bridge.ts` + `address-suggestions.ts` `selectAddressWithPlaceBinding` + `address-step`/`address-commit` 接线 - 执行记录 2:单测 axel-selection-bridge 3/3、address-step 3/3;待实网 `record-dual-address-compare` 验证 placeApiHitCount>0 ### Step 11.40 place commit 键盘主路径 + 闸门日志 - **根因(实网日志)**:`description-click` 命中 sc- 装饰 DOM,`placeNetworkLog=[]`,widget 未发 GET place - **修复**:`selectAddressWithPlaceBinding` 改 keyboard-bound-index 主路径;pac/listbox/全键盘回退;`formatPlaceCommitFailure` 闸门日志 - **状态**:Done(待 post-fix 实网验证) - 执行记录 1:debug-efb842 H2 CONFIRMED;H1/H3/H4/H5 REJECTED - 执行记录 2:`keyboard-bound-{index}` 优先;失败输出原因分类(无请求/placeId 不匹配/HTTP 非 200/超时) ### Step 11.41 RPA_ADDRESS_MODE 隔离 + AddressAdapter 架构 - **根因**:MotherShip selection handler 未触发 GET place,无法验证 cargo→quote 下游 - **修复**:`RPA_ADDRESS_MODE=real|mock`;`AddressAdapter` 封装 DOM;commit 状态机 + 分类诊断日志 - **状态**:Done - 执行记录 1:`lib/rpa/address-mode.ts` + `workers/rpa/address-adapter/*`;`address-step` 仅 `adapter.selectAddress()` - 执行记录 2:单测 12/12;`prove-rpa-chain` 记录 `RPA_ADDRESS_MODE` 并校验 mock 日志 ### Step 11.42 prove-rpa-chain 链路探通:跳过联想 API - **根因**:MotherShip 联想 API 超时阻塞链路验证;PowerShell 未正确设置 env - **修复**:`RPA_ADDRESS_MODE=mock` 或 `PROOF_SKIP_CANDIDATES=true` 时用 `proof-synthetic-candidates` 内置美国地址 - **状态**:Done - 执行记录 1:`scripts/lib/proof-synthetic-candidates.ts`;prove 跳过联想直 POST quotes - 执行记录 2:`.env` 默认 `RPA_ADDRESS_MODE=mock`;日志校验不再要求 candidates job ### Step 11.43 mock 链路探通:submit 闸门 + 快速填址 - **根因**:0029 超时因 Worker 未加载 mock;mock 仍调 ensureAddressSuggestions;submit 仍校验双地址锁定 - **修复**:mock 快速 fill 街道;fillCargo/submit/business-ready 跳过地址闸门;mock 后关闭 page 防 Browser 僵死 - **状态**:Done - 执行记录 1:`mock-address-adapter` 改 mock-fast-fill;`cargo-step`/`thin-browser-driver`/`quote-business-ready` mock 放行 - 执行记录 2:Worker 重启后 0029 已跑通 address+cargo,待 submit 复测 prove-rpa-chain ### Step 11.45 移除 fixture 占位价,回归 real 真实报价 - **根因**:mock + fixture 注入固定四档价,与 MotherShip 无关,人工对账必失败 - **修复**:删除 `mock-chain-quote-fixture`;`.env` 改 `RPA_ADDRESS_MODE=real`;prove/demo 走联想 API - **状态**:Done - 执行记录 1:`quote-network-capture.ts` 移除 fixture 注入;无 axel/quote 时 pipeline 失败 - 执行记录 2:下一步修 real 模式 place commit,确保 Worker 日志含真实 axel/quote 非 mock-chain-fixture ### Step 11.49 place commit 修复:Google 事件触发 + React 注入 - **依据**:`docs/修复计划.md` R1-R7 + A1-A6 + B1-B2 - **输出**:`workers/rpa/fix-place/*`;`RPA_PLACE_FIX_MODE`;`mothership-address-adapter` 策略前置;`address-step` pickup 稳定验证 - **逆向**:`.rpa/recon/verdict.json` → `PLAN_B`(headed 探测时 google.maps 未加载) - **状态**:Done - 执行记录 1:新增 fix-place 目录(google-event/react-state/strategy/recon-helpers)+ `scripts/recon-place-trigger.ts` + env 开关 - 执行记录 2:`fix-place/browser-eval-scripts.ts` 纯字符串 evaluate,修复 `__name is not defined`;global-rules §18 ### Step 11.52 Selection Trace 取证(不修交互) - **目标**:验证 combobox 状态机 / aria-activedescendant / 人工真实点击目标,而非继续猜 ArrowDown - **输出**:`workers/rpa/selection-trace.ts`;adapter events 含 `selection-trace:*`;`manual-selection-*.json` / `rpa-selection-trace.json` - **状态**:Done - 执行记录 1:候选出现后快照 activeElement、aria-activedescendant、pac、listbox;人工 click/keydown 监听 - 执行记录 2:单测 formatSelectionTraceEvent;待 manual vs rpa 对照 `combobox-dead` 与 `closestPacItem` ### Step 11.51 BLK-001 keyboard-bound ArrowDown 偏移 - **根因**:Google Places 默认高亮第 0 项;`keyboard-bound-0` 多按 1 次 ArrowDown 跳过唯一候选;`description-click` 先于键盘路径污染下拉状态 - **修复**:`arrowDownCountForPacIndex`;`alreadyHighlightedFirst` 默认 true;键盘路径优先于 DOM 点选;回退前 `ensureSuggestionDropdownOpen` - **状态**:Done - 执行记录 1:`address-suggestions.ts` keyboard-bound 主路径 + ArrowDown 计数修正 - 执行记录 2:单测 `arrowDownCountForPacIndex` 通过;待 `COMPARE_MODE=rpa` 组 99 验证 GET place/ChIJ… 200 ### Step 11.53 BLK-001 styled-components 联想点击主路径 - **根因**:MotherShip 使用 sc-* DIV 联想非 Google pac;ArrowDown 误触 Use my location;键盘路径触发错误 placeId - **修复**:`mothership-styled-suggestion-click.ts` 全页扫描+浏览器内 click;adapter 优先 styled 点击;禁用无 pac 时 keyboard-bound - **状态**:Done - 执行记录 1:新增 CLICK_MOTHERSHIP_STYLED_SUGGESTION、cookie dismiss、probe/demo-seattle-miami 脚本 - 执行记录 2:browser-place-commit 浏览器内 fetch 兜底;place-commit-strategy 接入 - **根因**:`fetchAxelPlaceLocation` 用 `page.evaluate(fetch)` → bridge 误判 `hasPlaceCommit`;`succeed()` 强制 `placeRequestObserved=true`;弱校验 `widgetContainsConfirmedAddress` 放行 pickup - **修复**:prefetch 改 `context.request.get`;`assertWidgetLockedAfterCommit` + `isPickupReadyForDeliveryInput` 闸门;`settlePickupBeforeDelivery` 前 diagnostic 日志 - **状态**:Done - 执行记录 1:`axel-place-prefetch.ts`/`mothership-address-adapter.ts`/`address-commit.ts`/`address-step.ts` - 执行记录 2:单测 axel-place-prefetch/bridge 通过;pickup 未锁定将在 adapter 层 fail-fast,不再误进 delivery - **根因**:prefetch 跳过 widget 双地址锁定;direct POST 仅 4 档且 lowest 偏低;demo 未传 quote_session_id - **修复**:ensureDualAddressCommitted 交替 refill;orchestrator 仅 widget submit;demo 五档断言 + session - **状态**:In progress - 执行记录 1:isPickupReady 去掉 dropdown 误判;forceDismiss + chip 态空派送框;fillAddress 后 releasePickup - 执行记录 2:enumerationMode 与 quote 路径分离;prefetch 后 Escape/Tab 锁定提货 chip ### Step 11.47 delivery 门牌绑定 + 枚举/query 对齐人工页 - **根因**:枚举首条即停;resolveSelection 用无门牌 display_label;direct quote 缺门牌 street - **修复**:多 query 直到含门牌;bindingLabels 优先完整街道;axel 候选过滤门牌;候选排序 - **状态**:Done - 执行记录 1:`enumerateAddressCandidates`/`ensureAddressSuggestions`/`enrichCandidateFromQuery`/`resolveSelection` 门牌优先 - 执行记录 2:`demo-screenshot-case-a` QTE_0051 delivery 已含 5678;lowest/bestValue 仍待 widget 双地址 commit + submit 路径 ### Step 11.46 placeId 归一化 + axel-place-prefetch + direct axel/quote - **根因**:复合 placeId 贪婪匹配;DOM 点选不触发 GET place;widget 双地址互斥 - **修复**:27 字符 ChIJ 截断;prefetch GET place;direct POST axel/quote;demo 实测两组不同价 - **状态**:Done - 执行记录 1:`normalizeAxelPlaceId`/`axel-place-prefetch.ts`/`axel-quote-direct.ts`;orchestrator 双地址缓存走 direct - 执行记录 2:demo QTE_0048/0049 source=rpa;Worker 日志 `direct axel/quote` 无 mock-chain-fixture ### Step 11.44 mock 全链路 PASS(已废弃 fixture 方案,见 11.45) - **根因**:MySQL 3307 宕机致 quote-create 500;worker-rpa.log UTF-16 致 prove 日志正则失败;L2 缓存撞 hash - **修复**:`dev:infra:start` 拉起 DB;prove 读日志 UTF-16 解码 + MySQL 探活 - **状态**:Superseded by 11.45 - 执行记录 1:QTE_0036 prove PASS 曾依赖 fixture,已撤销 - 执行记录 2:不得再使用 mock-chain-fixture 作为验收依据 ### Step 11.48 RPA 联想键入纯街道优先(POI 粘连 label) - **根因**:`address.street` 含 POI 全称导致 search 仅 1 条;`St`/`AvenueWest` 粘连使街道提取失败,联想下拉不出现 - **修复**:`extractStreetFromLabel` 粘连感知后缀;`resolveAddressAutocompleteInput` 替换 street;查询与 `ensureSuggestionDropdownOpen` 纯街道优先 - **状态**:Done - 执行记录 1:`address-suggestions.ts` 导出 `extractStreetFromLabel`/`resolveAddressAutocompleteInput`;`buildAddressAutocompleteQueries` 全量基于纯街道 - 执行记录 2:`address-suggestions.test.ts` 新增粘连 POI/Western Avenue 用例;单测 26 项通过 ### Step 11.54 直连 axel API 报价 CLI(抛弃 widget 交互) - **根因**:Google Places / styled-components 下拉对自动化不可穿透,widget 状态机无法推进 commit - **修复**:`lib/axel/*` Node fetch 客户端:search → place → quote;`npm run quote` CLI 输出 JSON - **状态**:Done - 执行记录 1:复用 `axel-selection-bridge` placeId 归一化;`buildAxelShipmentPayload` 与 HAR baseline 对齐 - 执行记录 2:`__tests__/lib/axel/quote.test.ts` 4 项通过;连续 3 次 CLI 同价验收 ### Step 11.55 embed-demo 接入直连 axel 报价 - **根因**:embed-demo 仍走 Playwright widget,地址 commit 失败无法询价 - **修复**:`RPA_QUOTE_MODE=direct`(默认)时 candidates/quote 均走 `lib/axel`;Worker 无需开浏览器 - **状态**:Done - 执行记录 1:`resolveAxelMothershipCandidates` + `fetchAxelQuoteItems` 接入 API 与 Worker - 执行记录 2:`mothership-candidates.test.ts` 更新 direct/widget 分支单测 ### Step 11.56 availableRates 兜底解析 - **根因**:部分路线 `data.rates.standard.lowest` 为空,仅 `availableRates` 有 customRate 定价 - **修复**:`decodeAxelQuoteBodyLenient` 在缺档时从 availableRates 按 serviceType/价格/时效补全 - **状态**:Done - 执行记录 1:`extractTiersFromAvailableRates` + `carrierInfo.name` 承运人解析 - 执行记录 2:重跑 batch D/F/H;单测 availableRates fallback 通过 ### Step 11.57 地址确认展示与不可选候选 - **根因**:已选择展示用 street/city/state 重组,与弹窗 display_label 不一致;部分 placeId GET place 失败仍可选 - **修复**:确认后展示 mothership_display_label;GET place 预校验 selectable;弹窗禁用不可选项 - **状态**:Done - 执行记录 1:`formatAddressDisplayLabel`/`axelSearchRowToCandidate`/`verifyCandidates` - 执行记录 2:`address-disambiguation-modal` 不可选提示;询价前二次校验 place ### Step 11.58 管理端客户加价配置(比例 / 固定金额) - **需求**:管理端按客户设置卡派报价加价,支持运费百分比或固定 USD 金额 - **输出**:`markup_type`/`markup_fixed_amount` 迁移;`modules/pricing/markup-*`;`/admin/markup` + `/api/admin/markup-configs` - **状态**:Done - 执行记录 1:加价引擎 `getMarkupRule` 支持 percent/fixed;L2/RPA 链路按客户重算 - 执行记录 2:`admin-markup-routes` + `markup-validation` 单测通过;`.env.example` 增 `CUSTOMER_REGISTRY` ### Step 11.59 货物详情限制口径调整 - **需求**:托盘≤25;尺寸 L≤999/W≤99/H≤99 in;单托重≤9999 lb - **输出**:`lib/constants/cargo-limits.ts`;`validation.ts`/`quote-form.tsx`;PRD §4.2.8 / domain-rules / 技术设计 - **状态**:Done - 执行记录 1:整票总重硬顶改为 249,975 lb(9999×25);校验增加单托重量上限 - 执行记录 2:`validation.test.ts` 边界用例 25 托/999×99×99 通过 ### Step 11.60 Widget 地址 commit 金样本多层回退 + 网络闸门 - **根因**:styled-components 联想 sc-* 类名动态;DOM 点选未触发 GET place/{placeId};verdict PLAN_B 仍走 google/react 注入 - **修复**:`gold-standard-targets` + `buildGoldDrivenLocators` 金样本文本/类名回退;`assertWidgetLockedAfterCommit` 要求 place 200 + 下拉收起;`place-commit-strategy` 按 verdict 跳过不可用路径;`address-step` 双地址闸门 - **状态**:Done - 执行记录 1:`mothership-styled-suggestion-click.ts`/`mothership-address-adapter.ts`/`place-commit-strategy.ts`/`address-step.ts`/`axel-selection-bridge.ts` - 执行记录 2:`probe:widget-address` 单测 17/17 通过;实网循环 `PROBE_WIDGET_LOOPS=3~10` + `prove:rpa-chain` 验收 ### Step 11.61 Widget 填址前保留下拉 + Cookie 遮罩 + browser-place evaluate - **根因**:`ensureAddressSuggestions` 成功后 `reopenStyledSuggestionDropdown` 清空输入致联想行消失;CookieYes revisit 抢焦点;`BROWSER_FETCH_AND_INJECT_PLACE` 为未调用 function 表达式 evaluate 返回 undefined;place 200 后 widget 未 chip 锁定即 fail - **修复**:先 `waitForStyledSuggestionRows`/`ensureSuggestionDropdownOpen` 再点选;`fillAddress` 入口 `stabilizeQuoteLandingPage`;Cookie 遮罩 CSS 隐藏;place 200 后 `lockPickupIntoChip`;`pickupLostAfterDelivery` 改 widget DOM 校验;`widgetContainsConfirmedAddress` 标签无门牌号时不强制;`assertDualAddressStable` 修复 combined 未定义 - **状态**:Done - 执行记录 1:`mothership-address-adapter.ts`/`page-prep.ts`/`browser-place-commit.ts`/`address-step.ts` - 执行记录 2:`browser-place-commit.test.ts` 单测通过;pickup chip 锁定闸门补全 ### Step 11.63 P0 实例拓扑 + 监听器指纹 + P2 fetch 劫持诊断 - **需求**:定位 place_changed 触发实例、listener 是否含 fetch、getPlace 完整性、未发 place 请求原因 - **输出**:`place-diagnostic-init.ts`/`place-diagnostic-probe.ts`/`scripts/probe-place-diagnostic.ts`;`RPA_PLACE_DIAG`;fix-place 阶段落盘 - **状态**:Done - 执行记录 1:P0 包装 maps.event.addListener/trigger + instance.addListener;P2 fetch/XHR 劫持 axel/location/place - 执行记录 2:`place-diagnostic.test.ts` 通过;`npm run probe:place-diagnostic` 可 headed 实网取证 - **根因**:旧方案仅 recon 脚本注入 Autocomplete hook;Playwright/evaluate 合成点击不触发 widget GET place;`event.trigger` 不保证 handler 执行 - **修复**:`cdp-click.ts`/`cdp-pac-commit.ts` Input.dispatchMouseEvent;`direct-listener-commit.ts` 直调捕获的 place_changed handler;`session-manager` 全局 `AUTOCOMPLETE_MONKEY_PATCH_INIT`;策略链 styled→cdp→direct-listener→browser-fetch→event→state - **状态**:Done - 执行记录 1:新增 fix-place/cdp-*、direct-listener-commit;recon-helpers 捕获 addListener;axel-place-prefetch 优先 CDP pac - 执行记录 2:place-commit-strategy/cdp-click 单测 7/7 通过;待实网 `prove:rpa-chain` 验证 placeApiHitCount≥2 ### Step 11.64 方案 A:双地址 vault 浏览器注入 + 随机地址报价探针 - **目标**:widget 路线通过 `addInitScript` 拦截 setState,保留 pickup;随机地址探针以拿到报价为成功标准 - **输出**:`workers/rpa/inject/dual-address-patch*`;`scripts/probe-random-address-quote.ts`;`RPA_DUAL_ADDRESS_PATCH` - **状态**:Paused(待真人录证结论,禁止继续堆策略) - 执行记录 1:`session-manager` 注入 init;`address-step` pickup/delivery 后 register+preserve - 执行记录 2:probe 仍卡在 assertPickupCommitted;转入 Step 11.65 真人录证 ### Step 11.65 真人完整链路录证(P0 冻结 fix-place) - **冻结**:禁止新增 fix-place/*、place-commit-strategy、selection-trace、新回退/新点击 - **必做**:HAR + 录像 + Playwright Trace(CDP) + Console + Network;定位 GET place/{id} 200 瞬间 - **输出**:`scripts/record-human-commit-baseline.ts`;`docs/真人录证-commit时刻.md` - **状态**:Done(脚本就绪;**5519e0ea 真人录证通过:2×place 200 + quote 200**) - 执行记录 1:新增 human-commit-baseline 子模块与四检查点录证流 - 执行记录 2:`5519e0ea` 分析见 `docs/mothership-commit-瞬间分析.md` ### Step 11.66 真人 vs RPA 网络差分脚本 - **输入**:`HUMAN_RUN_ID=5519e0ea` + 同地址 RPA adapter 填址 - **输出**:`compare-human-rpa-network.ts` + `network-diff.ts`;`rpa-diff-{id}/diff-report.json` - **状态**:Done - 执行记录 1:`npm run compare:human-rpa-network` 对比 search→place 链 - 执行记录 2:单测 `human-rpa-network-diff.test.ts`;禁止新增 fix-place ### Step 11.67 差分实跑(5519e0ea 同址) - **输入**:`HUMAN_RUN_ID=5519e0ea`;RPA 复用 `mothership-styled-click` 填址 - **输出**:`rpa-diff-7624e6b9/diff-report.json`;verdict=**MATCH**(place GET 200 各 2 次) - **状态**:Done - 执行记录 1:RPA pickup/delivery 均 `commitState=COMMITTED`;search→place 链 2/2 完整 - 执行记录 2:延迟差 ~9–10s(非阻塞);未跑 quote(差分脚本仅填址);下一步 `prove:rpa-chain` 同址全链 ### Step 11.68 差分脚本观测体验修复 - **现象**:首址前长时间无反应;填派送时界面跳动;填址成功后浏览器闪退 - **根因**:`waitForQuoterWidgetHydrated` 无进度日志;`waitForDeliveryStreetInput` 循环点分区+Escape/Tab;脚本 `finally` 强制 `closeBrowser` - **状态**:Done - 执行记录 1:`address-side` 派送框快路径;`compare-human-rpa-network` 尊重 `shouldKeepBrowserOpen`+阶段日志 - 执行记录 2:headed 默认 Ctrl+C 结束,非闪退;`RPA_DWELL_MS` 可选停留 ### Step 11.69 首址输入闪清修复 - **现象**:填第一个地址时界面已出现文案后突然清空 - **根因**:`triggerAddressAutocomplete`/`ensureSuggestionDropdownOpen` 无条件 `fill("")`;键入后 `scrollQuoteWidgetIntoView({force:true})` 扰动 widget - **状态**:Done - 执行记录 1:`addressInputMatchesQuery` 跳过冗余清空;滚动提前到键入前 - 执行记录 2:`openAddressSide` 返回同一 Locator,避免二次解析失焦 ### Step 11.70 派送填址加速 + 可视全链脚本 - **目标**:第二地址 search→place ~2s;可视验收地址→货物→报价 - **状态**:Done - 执行记录 1:`deliveryFast` 缩短 styled 等待 + `legacy-label-row-fast`;`visual-quote-full-chain.ts` - 执行记录 2:`RPA_FAST_CLICK`/`RPA_SLOW_MO_MS=0`;`npm run visual:quote-full-chain` ### Step 11.72 视觉优先双地址完成 → 直接填货物 - **需求**:不等 place 网络/下拉收起;widget 或地图已见双地址即进入 Freight details - **修复**:`isAddressesVisuallyCompleteForCargo`;`fillAddressStep`/`cargo-lock` 视觉放行 - **状态**:Done - 执行记录 1:`evaluateAddressesVisuallyComplete` 双州码/门牌/Freight details;delivery adapter 失败仍可走货物步 - 执行记录 2:`address-commit.test.ts` 截图态用例;`settleAddressFieldsForCargo` 视觉短路 - **现象**:从上到下扫 DOM;Freight details 弹出但不填值;脚本结束闪退 - **根因**:`removeAllFreightRows`/`tryLocatorStrategies` 循环点选;弹层 input 在 widget 外 - **状态**:Done - 执行记录 1:`fillLandingFreightPopover` 直填 How many pallets/Length/Weight;`legacyOnly` 单次点联想 - 执行记录 2:派送侧去掉 `waitForDeliveryStreetInput` 循环;`landingPopoverMatchesExpected` 放行 commit ### Step 11.73 sign-up-quote 报价页 lenient 解析 - **需求**:报价页已出现;axel/quote 有数据但 rates.* 叶节点不全,禁止 assertQuoteContract 硬拦 - **修复**:`decodeQuotePayloads` 契约失败时走 `decodeAxelQuoteBodyLenient`(availableRates 推断档位) - **状态**:Done - 执行记录 1:`tryLenientAxelDecode` 识别 sign-up-quote + axel/quote URL;source=lenient - 执行记录 2:`quote-capture.test.ts` 契约不全 lenient 用例;移除 assertQuoteContract 抛错路径 ### Step 11.74 可视冲刺模式 RPA_VISUAL_RUSH - **需求**:填址点选联想 + 填货物后直接点报价,不等待锁定/契约/后端检测 - **修复**:`isRpaVisualRushMode`;address/cargo/submit/pipeline 跳过闸门;关闭 dual-address-patch - **状态**:Done - 执行记录 1:`visual-quote-full-chain` 默认 `RPA_VISUAL_RUSH=true`;`submitQuoteForm` 直点 Get a freight quote - 执行记录 2:pickup/delivery 均取消 `place`/`legacy` 长等待;联想点击后直接继续 ### Step 11.75 Direct 优先 / Widget 兜底报价策略 - **需求**:Direct 为主路径;仅 Direct 明确失败后才允许 Widget;禁止并行/预热/默认 Widget - **修复**:`quote-strategy.ts` + `orchestrator`/`mothership` 串联;`pipeline` 闸门 `widgetFallbackAfterDirectFailure` - **状态**:Done - 执行记录 1:node `fetchAxelQuoteItems` → 浏览器 kernel `captureQuotesDirectFirst`;`RPA_DISABLE_WIDGET_QUOTE_FALLBACK` 可关 fallback - 执行记录 2:`quote-strategy.test.ts` + pipeline gate;脚本与 `.env` 移除 `RPA_QUOTE_MODE=widget` 默认 ### Step 11.76 人工 docx 系统全链路探针 - **需求**:不经 RPA 代码直调,走 POST /api/quotes 系统链路验证 docx 10 组 - **输出**:`scripts/probe-human-doc-system.ts` + `npm run probe:human-doc-system` - **状态**:Done - 执行记录 1:`scripts/lib/human-doc-10pairs.ts` 共享地址;候选→创建→轮询 GET - 执行记录 2:H01 系统探针 PASS(4 档 rpa);全量 10 组后台跑 `.rpa/human-doc-system-probe/` ### Step 11.77 系统探针失败组地址误匹配修复 - **现象**:H06/H07 Bridge 误点;H08 city-only;H09/H10 队列饥饿 - **修复**:`suggestionLabelConflicts` + 禁用 preknown 视觉放行 + 键盘索引优先 + 探针候选打分 - **状态**:Done - 执行记录 1:`mothership-styled-suggestion-click.ts` 拒绝 Bridge/sc-fallback;`address-commit` 桥接校验 - 执行记录 2:`mothership-address-adapter` preknown-keyboard;探针 `pickBestCandidate` + 轮询 600s ### Step 11.78 系统探针失败组修复验证 - **现象**:H06-H10 place_id_mismatch(Bridge 误点);preknown synthetic 被闸门拦截 - **修复**:preknown 跳过 legacy 点选 + synthetic 放宽闸门 + dev 重启加载新代码 - **状态**:Done - 执行记录 1:`npm run probe:human-doc-system` 10/10 PASS(`system-e2583d51.json`) - 执行记录 2:探针 `uniqueWeight` 防 cache 空档;H06/H07 单测 12–17s 通过 ### Step 11.79 代码库清理(冗余文件与过时文档) - **目标**:合并重复数据、删除废弃 re-export 与一次性 debug 脚本、更新项目索引 - **状态**:Done - 执行记录 1:删除 `docs/修复计划.md`、20 个 `debug-*.ts`、`browser-eval-scripts.ts`、`event-capture.ts` - 执行记录 2:共享 `human-doc-10pairs`/`pick-mothership-candidate`;更新 `查价项目.md` 与 `verify-address-candidates` ### Step 11.80 availableRates 补全缺失档位(非仅缺 lowest 时) - **根因**:`decodeAxelQuoteBodyLenient` 在 `rates.*` 已有 standard/lowest 时跳过 `availableRates`,导致 standard.bestValue 等扩展档丢失(2+1 vs 人工 3+1) - **修复**:`mergePartials` 始终合并 `availableRates` 中缺失档,不覆盖已有 `rates.*` 叶节点 finalPrice - **状态**:Done - 执行记录 1:`payload-decode.ts` `extractFromContractBodies` + `decodeAxelQuoteBodyLenient` - 执行记录 2:单测「rates 已有 lowest 仍补全 bestValue」通过 ### Step 11.81 禁止 availableRates customRate 伪造 UI 档位名 - **根因**:Step 11.80 无条件合并 `availableRates`;customRate 池按价/时效推断 lowest/fastest/bestValue,在 `rates.*` 已有 3+1 时膨胀为 6 档且名称与价格错位(QTE_20260626_0045) - **修复**:档位名仅以 `rates.{standard|guaranteed}` 下子键为准;customRate 推断仅当该 serviceLevel 块无 UI 子键;显式 serviceType 亦受槽位白名单约束 - **状态**:Done - 执行记录 1:`listUiRateOptionsFromRatesBlock` + `extractTiersFromAvailableRates(body, rates)` - 执行记录 2:单测「3+1 禁止伪 guaranteed/lowest|fastest」通过 ### Step 11.82 fastest 档位 id 关联 + 显式 serviceType 补档 - **根因**:`rates.standard.fastest` 常为 `{ id }` 引用无 finalPrice;或最快档仅在 `availableRates` 显式 `serviceType: fastest`;旧逻辑只读叶节点导致缺 $768.88 - **修复**:`resolveTierFromRatesSlot` 经 id 查 `availableRates`;standard 允许显式 serviceType 补缺失档;guaranteed 仍仅 `rates.guaranteed` 子键 - **状态**:Done - 执行记录 1:`extractTiersFromRates(rates, availableRates)` + `listAllowedRateOptionsForSupplement` - 执行记录 2:单测 id 引用 fastest / 显式 fastest 补档通过