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