# 查价项目

> **新人入口文档**：5 分钟了解项目、跑起来、别踩红线。  
> 权威细节以 PRD / 技术设计 / 工程任务拆解为准；本文是索引与速查。

| 项 | 值 |
|----|-----|
| 文档版本 | v1.3 |
| 更新日期 | 2026-06-30 |
| 产品名 | 美美与共报价中台（查价系统） |
| 代码仓库 | `chajia` |

---

## AI 项目交接文档

> 供下一 AI 会话快速接手。执行日志：`cursor_project_rules/implementation-plan.mdc`（Step 11.75~11.86）。

### 1. 项目核心身份 (Project Identity)

**项目名称与一句话定义**

**美美与共报价中台（chajia）** — 通过 Playwright RPA 抓取 MotherShip 公开报价页，一次询价返回多档 LTL 报价，经加价引擎输出给宿主系统内嵌查价组件；中台自身提供 admin 管理端（预警、RPA 运维、队列监控）。

**技术栈清单**（来源：`package.json` / `docker-compose.yml`）

| 类别 | 技术 | 版本 |
|------|------|------|
| 语言 | TypeScript | ^5.9.2 |
| 运行时 | Node.js | [需人工补充: 最低 Node 版本] |
| Web 框架 | Next.js (App Router) | ^15.5.6 |
| UI | React | ^19.1.1 |
| 样式 | Tailwind CSS | ^3.4.19 |
| 组件 | shadcn/ui 风格 + Geist 字体 + @phosphor-icons/react ^2.1.10 | — |
| 表单 | react-hook-form ^7.79.0 + zod ^3.25.76 | — |
| API | Next.js Route Handlers | — |
| ORM | Prisma | ^6.19.0 |
| 数据库 | MySQL | 8.0（docker image `mysql:8.0`） |
| 缓存/队列 | Redis 7-alpine + ioredis ^5.8.1 + BullMQ ^5.78.1 | 本机 WSL 实测 6.0.16 有警告 |
| RPA | Playwright ^1.61.0；可选 playwright-extra + stealth | — |
| 队列 UI | Bull Board (@bull-board/* ^8.0.0) + Hono ^4.12.25 | — |
| 鉴权 | jose ^5.10.0 + bcryptjs ^3.0.2 | — |
| 测试 | Vitest ^3.2.4 + @playwright/test ^1.61.0 | — |
| 构建/脚本 | tsx ^4.20.5 | — |
| 部署 | Docker Compose（全栈或宝塔宿主机 MySQL + Docker 应用） | 见 `deploy/` |

**目录结构语义化说明**

| 路径 | 职责 | 数据流向 |
|------|------|----------|
| `app/` | Next.js 页面与 API Routes（薄路由层） | 宿主/Admin HTTP → `modules/*` |
| `app/api/quotes/` | 询价创建与轮询（方式 B） | → `modules/quote/orchestrator` → BullMQ |
| `app/api/host/quote/` | 宿主两步公开接口（方式 A，免 Token） | candidates → submit（服务端内部轮询） |
| `app/api/addresses/mothership-candidates/` | 地址候选（方式 B 第 1 步） | → BullMQ `address-candidates` job |
| `lib/axel/` | MotherShip axel HTTP 直连（search/place/quote） | `buildAxelCargo` 整数磅/英寸 |
| `lib/constants/host-service.ts` | 默认 Service Token / customer_id | embed-demo 与内网对接 |
| `components/` | 宿主内嵌查价 UI、管理端组件 | hooks → API |
| `modules/quote/` | 询价编排、校验、幂等、fallback | L1→L2→RPA→L3 状态机 |
| `modules/pricing/` | 加价引擎（仅 raw_freight，0~30%） | 读 MarkupConfig → 写 QuoteRecord |
| `modules/cache/` | Redis L1/L2/L3、限流、熔断 | 读写 Redis |
| `workers/rpa/` | BullMQ Consumer + Playwright 填表/抓价 | 消费 queue → 写 MySQL/Redis |
| `workers/rpa/quote-capture/` | Network-First 报价捕获（axel/quote 契约解析） | submit 后监听 network → normalize |
| `workers/rpa/address-adapter/` | 地址选择抽象（real/mock） | DOM + axel search/place API |
| `workers/rpa/kernel/` | RPA 步骤编排（address/cargo/quote steps） | 内部状态机 |
| `lib/` | prisma/redis/response/rpa env 等基础设施 | 被 app/modules/workers 引用 |
| `prisma/` | Schema、migrations、seed | MySQL 持久化 |
| `scripts/` | probe、prove、smoke、录证、准确度测试 | CLI 验收/排障 |
| `cursor_project_rules/` | AI 编码规则 + implementation-plan 逐步日志 | 必读 |
| `.rpa/` | storageState、session、chain-proof 产物 | 勿提交敏感态（gitignore） |
| `.dev/logs/` | worker/next 运行日志、失败截图 | 排障 |

**端到端数据流**

```
【方式 A — 宿主公开接口，推荐】
宿主前端
  → POST /api/host/quote/candidates（地址+货物 → RPA 枚举候选 + host_session_id）
  → 用户选候选
  → POST /api/host/quote/submit（服务端内部轮询，同步返回最终报价）

【方式 B — Token 三步，embed-demo / 后期鉴权】
EmbeddedQuoteWidget
  → POST /api/addresses/mothership-candidates
  → 用户确认弹窗
  → POST /api/quotes → L1/L2 未命中 → BullMQ quote-rpa
  → workers/rpa：Direct 优先 node fetchAxelQuoteItems → 失败可 Widget fallback（生产建议关闭）
  → 写 L2/L3 + quote_record → GET /api/quotes/{id} 轮询
```

---

### 2. 当前开发状态快照 (Current State Snapshot)

**已完成模块**（单测/集成/E2E 在 Mock 或隔离路径下通过）

| 模块 | 验收依据 |
|------|----------|
| Phase 0~4 底座 | M1：POST /quotes、L2 缓存、幂等、校验 400 |
| 鉴权 RBAC + Service Token | middleware + 单测 |
| 三级缓存 + 限流 + 熔断 | redis-cache / rate-limiter 单测 |
| 加价引擎 | engine.test.ts TC-103 |
| 询价编排 + fallback + 超时 sweeper | orchestrator / timeout-sweeper 单测 |
| BullMQ 队列 + Worker 管理 API | rpa status/pause |
| 宿主内嵌查价 UI + 地址确认弹窗 | embed-demo + E2E |
| 管理端（预警/RPA/队列/指标/客户加价） | admin 路由 |
| 宿主公开 API（方式 A） | `app/api/host/quote/*` + `deploy/第三方对接指南.md` |
| 内网生产部署（宝塔 MySQL） | `deploy/docker-compose.bt-host-mysql.yml` + `install-bt-host-mysql.sh` |
| axel 公制取整 | `toAxelWholeInches` / `toAxelWholePounds`（直连 API 边界） |
| CI lint + test:unit + test:integration + build | `.github/workflows/ci.yml` |
| RPA v1.2 骨架 | QuotePageAdapter、selector env、Network-First capture、contract guard |
| 阶段 0 录证 | `.rpa/phase0/quote-contract-summary.json`：headed 人工可拿到 axel/quote |
| 货物填表（mock 路径） | fillCargoInDrawer + cargo-reset 单测 |

**进行中任务**

| 任务 | 进度 | 说明 |
|------|------|------|
| **M2-v1.2** 真实 RPA probe 3/3 | **待跑** | `npm run probe:rpa:3x` |
| **内网生产验收** | **进行中** | 宝塔 MySQL + `192.168.2.14:30325`；公制货物 + Direct 模式 |
| task-133~135 CI probe 门禁 | 待执行 | TC-601 |
| Priority1 FTL/LTL 录制与 benchmark | **实验** | 非 MVP；`visual:priority1-*` / `record:priority1-*` |

**最近 3 次关键变更**

| 次序 | 变更 | 原因 | 影响模块 |
|------|------|------|----------|
| 1 | **宿主公开 API**（`/api/host/quote/candidates` + `submit`） | 美美与共等宿主免 Token、服务端同步返回报价 | `app/api/host/`、`deploy/API.md` |
| 2 | **生产 Direct 报价 + 关闭 Widget fallback** | 避免浏览器回退卡 6~12 分钟；内网快速失败 | `quote-strategy.ts`、`RPA_QUOTE_MODE=direct`、`RPA_DISABLE_WIDGET_QUOTE_FALLBACK=true` |
| 3 | **axel 公制取整**（磅/英寸 `Math.ceil`） | MotherShip `axel/quote` 要求整数；公制 API 会算出小数 | `unit-converter.ts`、`lib/axel/shipment.ts` |

---

### 3. 问题与阻塞项登记 (Issues & Blockers Registry)

> **2026-06-30 状态**：人工 docx 10 组 `probe:human-doc-system` **10/10 PASS**。内网已部署宝塔 MySQL + Docker 应用（`30325`）；生产模板默认 Direct + 禁 Widget fallback。公制 API 须走 `toAxelWhole*` 取整后发 axel。

| 问题ID | 状态 | 问题描述 | 复现路径/报错信息 | 已尝试的解决方案(含失败原因) | 下一步建议方案 | 相关文件路径 |
|--------|------|----------|-------------------|------------------------------|----------------|--------------|
| **BLK-012** | 已修复 | **P0** 公制货物直连 axel HTTP 400（磅/英寸须整数） | API 传 500kg / 120cm → `Weight/Length must be a whole number` | 内部 GD-13 保留 2 位小数；边界 `toAxelWholePounds/Inches` 向上取整 | 验收公制样例 + 英制默认样例均 PASS | `unit-converter.ts`、`lib/axel/shipment.ts` |
| **BLK-013** | 已修复 | 打包后 `GET /api/quotes/{id}` 返回 HTML 404 | Windows 打包脚本 `[quote_id]` 被当通配符 | `pack-server-upload.ps1` 使用 `-LiteralPath`；安装脚本预检路由 | 上传后 `curl` 确认 JSON 非 HTML | `deploy/pack-server-upload.ps1` |
| **BLK-014** | 已缓解 | Docker 部署 next-app 缺 Playwright / storageState | 503 或 bootstrap 失败 | `rpa_state` 卷共享；next-app `RPA_AUTO_BOOTSTRAP_STORAGE=false` | worker 负责 bootstrap；两容器共享 `.rpa` | `docker-compose.bt-host-mysql.yml` |
| **BLK-001** | 已缓解 | **P0** 地址 DOM 点选不触发 `GET place/{placeId}`，commit 失败 | 历史：embed-demo → worker `no_place_request` | description-click / keyboard / pac-dispatch 等；**11.77** preknown 跳过 legacy 点选、走 synthetic | 新地址回归 `probe:human-doc-system`；headed 录证见 `docs/真人录证-commit时刻.md` | `mothership-address-adapter.ts`、`address-commit.ts` |
| **BLK-002** | 已缓解 | **P0** 填 delivery 后 pickup 丢失（widget 互斥） | 历史：dual-address `pickupLostAfterDelivery=true` | 交替 refill（11.48）；preknown synthetic（11.77） | 同上 | `address-side.ts`、`address-commit.ts` |
| **BLK-003** | 已缓解 | submit 后无 `axel/quote`（contract=0） | 历史：commit 失败导致无 quote | 根因是地址未 commit；Direct-first + Widget fallback（11.75） | 监控 worker 日志 `axel/quote`；勿在 commit 通前改 contract 层 | `quote-capture/pipeline.ts`、`quote-strategy.ts` |
| **BLK-004** | 已关闭 | mock/fixture 假通过误导验收 | `prove:rpa-chain` PASS 但价非 MotherShip | mock-chain-fixture **已删除（11.45）** | 验收 `RPA_ADDRESS_MODE=real` + 无 fixture | `.rpa/chain-proof-72b4e980.json`（历史样例） |
| **BLK-005** | 已更新 | direct POST axel/quote 价与 UI 不一致 | 历史：direct 价偏低 | **11.75** Direct 优先、失败再 Widget fallback | 勿单独启用 Widget 并行/预热 | `quote-strategy.ts`、`axel-quote-direct.ts` |
| **BLK-006** | 开放 | L2 缓存假阳性 | `创建即 done + source_type=cache` | 探针用 `uniqueWeight` 防命中空 cache | 验收前换 cargo 或 `FLUSHDB` | `modules/cache/redis-cache.ts` |
| **BLK-007** | 开放 | 端到端超时 420s | commit 失败时前端超时 | 提 timeout 不能解决 commit；commit 通后约 2–3 分钟/单 | commit 稳定后评估 browser warm/session | `lib/constants/quote.ts` |
| **BLK-008** | 开放 | WSL Redis/MySQL 间歇断连 | worker `ECONNREFUSED 6379/3307` | keepalive + orphan 回收 | 长跑前 `npm run dev:infra:start` | `lib/redis.ts`、`queue-recovery.ts` |
| **BLK-009** | 开放 | 候选 API 60~180s 慢 | POST `/api/addresses/mothership-candidates` | 枚举优化、Worker 队列 | 候选慢可接受；`pickBestMothershipCandidate` 降权 Bridge | `address-candidates.ts`、`pick-mothership-candidate.ts` |
| **BLK-010** | 开放 | storageState 货物残留 | 摘要 2296Lbs vs 期望 500Lbs | freshCargoContext + cargo-reset | real 路径验证 `assertWidgetCargoSummary` | `cargo-reset.ts`、`session-manager.ts` |
| **BLK-011** | 冻结 | 架构过度重构拖慢排障 | kernel/驻留页多轮 | 11.30 放弃驻留页默认路径 | **冻结架构**；仅改 adapter + commit | `workers/rpa/kernel/` |

**根因链（历史 → 当前）**

```
[历史] placeApiHitCount=0 → commit FAILED → 无 axel/quote → 420s 超时
[当前] Direct-first + 系统探针 10/10；内网宝塔部署 + 公制取整；宿主方式 A 两步 API
```

---

### 4. 架构决策记录 (ADR)

**ADR-1：Network-First 报价捕获（禁止 DOM 刮价）**

- **决策**：报价仅来自 `services.mothership.com/axel/quote` 网络响应，经 `quote-contract-guard` + `payload-decode` 解析；禁止 DOM price extraction。
- **原因**：MotherShip SPA 档位与 DOM 不同步；阶段 0 HAR 已验证 API 契约稳定。
- **权衡**：放弃 DOM fallback；submit 后必须触发真实 quote 请求，**强依赖地址/货物 commit 成功**。

**ADR-2：BullMQ 统一 RPA Worker（候选 + 询价同进程）**

- **决策**：地址候选与询价均入 BullMQ，同一 `workers/rpa/index.ts` 消费；`withRpaSessionLock` 串行。
- **原因**：spawn 子进程与 worker 双浏览器争用 `.rpa/mothership-storage.json`，行为不一致。
- **权衡**：单 worker 吞吐低；Worker 不可用时才 spawn 回退（stall 60s）。

**ADR-3：MotherShip 地址须用户确认 + place API commit**

- **决策**：RPA 枚举候选 → 前端弹窗确认 → 询价时按 `display_label` 点选；commit 以 `GET place/{placeId}` 网络证据为准。
- **原因**：PRD GD 要求点选；仅 DOM 填值不满足 MotherShip widget 状态机。
- **权衡**：端到端 180~360s；**当前 place commit 未打通即全链路失败**。

**ADR-4：Next.js Route Handlers 薄层 + modules 业务编排**

- **决策**：API Route 只鉴权/校验/调 orchestrator；禁止 Route 内写复杂业务。
- **原因**：可测试性、分层清晰、Worker 与 API 共享 modules。
- **权衡**：无 Server Actions；宿主必须 HTTP 集成。

**ADR-5：放弃驻留页默认路径（`RPA_PARKED_SESSION=false`）**

- **决策**：候选完成后不默认 park page；询价 job 独立开页 + storageState；browser warm 保留。
- **原因**：驻留页需 worker+confirm+同进程，验证周期 >5min 且 fetch failed 频发；网络等待占 80%+。
- **权衡**：候选→询价无法零重开；性能优化有限。

**ADR-6：生产环境 Direct 优先、默认关闭 Widget fallback**

- **决策**：`RPA_QUOTE_MODE=direct`；`RPA_DISABLE_WIDGET_QUOTE_FALLBACK=true`（生产模板）；node `fetchAxelQuoteItems` 为主路径。
- **原因**：Widget 浏览器填表回退单次 6~12 分钟且 worker 并发为 1，易阻塞队列。
- **权衡**：直连对 payload 更严格（整数磅/英寸、有效 placeId）；失败须快速报错而非长时间挂起。

**ADR-7：内部精度与 axel API 边界分离**

- **决策**：`kgToLb` / `cmToIn` 内部保留 2 位小数（GD-13 哈希/展示）；调用 MotherShip `axel/quote` 前 `toAxelWholePounds` / `toAxelWholeInches` 向上取整。
- **原因**：embed-demo 默认英制整数不易暴露；公制 API / 宿主对接会触发小数。
- **权衡**：取整仅影响 MotherShip 边界，不改变 L2 cache key 算法（仍用内部精度）。

---

### 5. 新 AI 接手指令 (Onboarding Instructions)

**启动/运行命令**

```bash
cp .env.example .env                    # 首次
npm run dev:infra:setup                 # WSL MySQL+Redis（Windows 推荐）
npm run dev:start                       # infra + migrate + seed + next + worker + scheduler

# 分项
npm run dev                             # Next.js :3000
npm run worker:rpa                      # RPA Worker（另开终端）
npm run worker:scheduler                # 超时清扫

# 验证
npm run smoke:connect                   # MySQL/Redis
npm run verify:rpa-stack                # 四步全栈验收
npm run prove:rpa-chain                 # 端到端 → .rpa/chain-proof-*.json
RPA_HEADLESS=false npm run probe:rpa    # headed RPA 调试

# 测试
npm run lint
npm run test:unit
npm run test:integration
npm run test:e2e
npm run go-no-go -- --mock

# 停止
npm run dev:stop
```

**环境变量依赖**（名称为准，勿写值）

| 变量 | 用途 |
|------|------|
| `DATABASE_URL` | MySQL 连接（本地通常端口 3307） |
| `REDIS_URL` | Redis 连接 |
| `JWT_SECRET` | 管理端 JWT |
| `HOST_SERVICE_TOKENS` | 宿主 Service Token → customer_id（内网默认 `chajia-neibu-2026`） |
| `HOST_PUBLIC_API_ENABLED` | `true` 启用方式 A 公开接口（免 Token） |
| `HOST_PUBLIC_DEFAULT_CUSTOMER_ID` | 方式 A 默认客户 ID |
| `RPA_QUOTE_MODE` | `direct`（生产）/ `widget`（调试） |
| `RPA_DISABLE_WIDGET_QUOTE_FALLBACK` | 生产建议 `true`，直连失败不浏览器回退 |
| `RPA_AUTO_BOOTSTRAP_STORAGE` | worker `true`；next-app 容器建议 `false` |
| `MOTHERSHIP_QUOTE_URLS` | RPA 入口 URL 列表（必填，禁止 dashboard） |
| `RPA_SELECTOR_*` | 8 项 Playwright selector（PICKUP/DELIVERY/WIDGET/STREET/SUGGESTION/CARGO/SUBMIT 等） |
| `RPA_STORAGE_STATE_PATH` | Playwright storageState 路径 |
| `RPA_MOCK_MODE` | `false`=真实 RPA；`true` 仅 CI/单测 |
| `RPA_ADDRESS_MODE` | `real` / `mock`（mock 不能用于验收） |
| `RPA_HEADLESS` / `RPA_HEADED` | 有头调试 |
| `RPA_BROWSER_LOCALE` | 推荐 `en-US` |
| `RPA_BROWSER_CHANNEL` | 推荐 `chrome` |
| `RPA_USE_STEALTH` | 默认 `false`（曾破坏 widget） |
| `RPA_PARKED_SESSION` | 默认 `false` |
| `RPA_QUEUE_ENABLED` | Worker 是否消费队列 |
| `RPA_WORKER_ID` | Worker 标识（空则自动生成） |
| `MOTHERSHIP_QUOTE_API_URL_PATTERNS` | axel/quote telemetry 白名单 |
| `MOTHERSHIP_EMAIL` / `MOTHERSHIP_PASSWORD` | 仅 login 页时需要 |
| `DEV_INFRA_MODE` | `native` / `docker` / `auto` |
| `MYSQL_HOST_PORT` / `MYSQL_ROOT_PASSWORD` | Docker/WSL MySQL |

**首要任务优先级**

1. **P0** 回归门禁：`npm run probe:human-doc-system` 10/10（改地址/RPA 后必跑；worker 需 `dev:stop` + `dev:start` 重载）
2. **P0** 实网验收：`npm run prove:rpa-chain` 在 `RPA_ADDRESS_MODE=real` 下 PASS（非 cache、非 fixture）
3. **P1** embed-demo 标准样例（LA 90001 → Dallas 75201，2 托 500lb）返回 `source_type=rpa` 且价与 MotherShip 页面一致
4. **P2** M2 门禁：`npm run probe:rpa:3x` 连续 exit 0
5. **禁止**：在 commit 未通前改 timeout/stealth/contract/kernel/驻留页；禁止 Widget 并行/预热/默认路径

**项目特殊约定**

| 类别 | 约定 |
|------|------|
| 编码前必读 | `cursor_project_rules/global-rules.mdc`、`implementation-plan.mdc`、`domain-rules.mdc` |
| 分层 | `app/api` 薄路由 → `modules` 业务 → `workers/rpa` 长任务 |
| 命名 | DB snake_case；TS camelCase/PascalCase；env UPPER_SNAKE |
| 金额 | DECIMAL(12,2)；`ROUND_HALF_UP`；禁止 float 运算 |
| 界面文案 | **全部中文**（MotherShip 抓取字段除外） |
| 红线 | 一次询价=1 次 RPA；cache key 不含 service_level；禁止 dashboard URL；禁止 RPA mock 上线 |
| Git | 禁止 main 直推；不提交 `.env`/`.rpa/` 敏感态 |
| 完成步骤 | 在 `implementation-plan.mdc` 写 `Done` + 两行执行记录 |
| 禁止库/模式 | 禁止 DOM 刮价；禁止 mock-chain-fixture；禁止 default dashboard URL；禁止空 catch |
| Windows 开发 | 优先 `npm run dev:start`（PowerShell）；WSL native infra 优于 Docker Desktop |

**关键产物路径**

| 路径 | 内容 |
|------|------|
| `.dev/logs/worker-rpa.log` | Worker 实网日志 |
| `.dev/logs/rpa-failure-*.png` | RPA 失败截图 |
| `.rpa/chain-proof-*.json` | prove 链路报告 |
| `.rpa/dual-address-compare-*.json` | 双地址 network 对比 |
| `.rpa/phase0/quote-contract-summary.json` | axel/quote API 契约 |

---

## 项目信息

### 做什么

通过 **Playwright RPA** 抓取 Mothership 公开报价页，一次询价返回 **4 档**报价（standard/guaranteed × lowest/fastest），经加价引擎输出给**宿主系统**内嵌查价组件。中台自身仅提供 **admin 管理端**（预警、RPA 状态、队列监控）。

### 不做什么（MVP 边界）

- 不使用 Mothership 官方 API
- 无独立客户/运营登录、无运营加价 UI
- 无 CC 对接、多币种、下单、对账、Priority1

### 技术栈

| 层 | 选型 |
|----|------|
| Web | Next.js 15（App Router）+ React 19 + Tailwind + shadcn/ui |
| API | Next.js Route Handlers |
| 数据 | MySQL 8 + Prisma |
| 缓存/队列 | Redis 7 + BullMQ |
| RPA | Playwright（可选 Patchright） |
| 测试 | Vitest + Playwright E2E |
| 部署 | Docker Compose（全栈 / 宝塔宿主机 MySQL） |

### 核心文档（按阅读顺序）

| 文档 | 用途 |
|------|------|
| [查价系统-PRD.md](./查价系统-PRD.md) v0.6 | 业务规则、GD 决策、验收标准（**最高业务权威**） |
| [查价系统-技术设计.md](./查价系统-技术设计.md) v1.2 | 架构、目录、API、RPA 规格（**实现权威**） |
| [查价系统-工程任务拆解-v1.2.md](./查价系统-工程任务拆解-v1.2.md) | 原子任务与开发顺序 |
| [查价系统-UI设计.md](./查价系统-UI设计.md) | 页面、交互、状态机 |
| [参考-Mothership-RPA-开源借鉴与解阻塞.md](./参考-Mothership-RPA-开源借鉴与解阻塞.md) | RPA 排障参考 |
| [deploy/README.md](../deploy/README.md) | 全 Docker 生产部署 |
| [deploy/宝塔部署说明.md](../deploy/宝塔部署说明.md) | 宝塔 MySQL + Docker 应用（内网 30325） |
| [deploy/API.md](../deploy/API.md) | 外部系统接口速查 |
| [deploy/第三方对接指南.md](../deploy/第三方对接指南.md) | 宿主对接完整指南（方式 A/B） |
| [报价提速与稳定性-改动清单.md](./报价提速与稳定性-改动清单.md) | 提速/稳定性分批实施清单（Phase 12） |
| [AI 项目交接文档](./查价项目.md#ai-项目交接文档) | 完整 handoff：身份/状态/阻塞/ADR/接手指令 | **切换 AI 必读** |
| `cursor_project_rules/` | AI/人工编码规则与实施日志 | implementation-plan 逐步记录 |

### 目录结构（关键路径）

```
chajia/
├── app/                    # Next.js 页面 + API Routes
├── components/             # 前端组件（含宿主内嵌查价）
├── modules/                # 业务逻辑（quote/cache/pricing/alert）
├── workers/rpa/            # BullMQ Consumer + Playwright
├── lib/                    # prisma/redis/response/rpa
├── prisma/                 # schema + migrations + seed
├── scripts/                # probe、smoke、go-no-go、录制
├── __tests__/              # unit / integration / e2e
├── docs/                   # 产品与工程文档
└── cursor_project_rules/   # 项目规则 + implementation-plan
```

### 角色与鉴权

| 角色 | 方式 | 能力 |
|------|------|------|
| 宿主系统 | `Authorization: Bearer <HOST_SERVICE_TOKEN>` | 代 `customer_id` 询价、历史、加价配置 |
| 管理员 | JWT + RBAC | 预警、RPA 运维、`/admin/*` |

联调样例：

- 内网宿主 token：`chajia-neibu-2026` → `CUST_001`（见 `lib/constants/host-service.ts`）
- embed-demo 亦使用上述 token（构建时 `NEXT_PUBLIC_EMBED_DEMO_*` 可覆盖）
- 管理端：`admin_demo` / `Demo@123`（仅开发环境）

**宿主对接方式**

| 方式 | 条件 | 接口 |
|------|------|------|
| **A（推荐）** | `HOST_PUBLIC_API_ENABLED=true` | `POST /api/host/quote/candidates` → `POST /api/host/quote/submit`（免 Token，服务端同步返回） |
| **B** | `HOST_PUBLIC_API_ENABLED=false` + Bearer Token | 候选 → `POST /api/quotes` → `GET /api/quotes/{id}` 轮询 |

详见 [deploy/第三方对接指南.md](../deploy/第三方对接指南.md)。

### 联调标准样例

**embed-demo 默认（英制，不易触发 axel 整数校验）**

| 字段 | 值 |
|------|-----|
| 路线 | LA(90001) → Dallas(75201) |
| 单托重量 | 500 lb |
| 单托尺寸 | 48×40×48 in |
| 托盘数 | 2 |
| 货物类型 | `general_freight` |

**宿主 API / 公制验收样例（须走取整后发 axel）**

| 字段 | 值 |
|------|-----|
| 单托重量 | 500 kg → 1103 lb（向上取整） |
| 单托尺寸 | 120×100×150 cm → 48×40×60 in（各边向上取整） |
| 托盘数 | 2 |

---

## 常用命令

### 环境准备

```bash
# 复制环境变量（勿提交 .env）
cp .env.example .env

# 一键启动（推荐，含 infra + app + worker）
npm run dev:start

# 仅 Docker infra（MySQL + Redis）
docker compose up -d mysql redis

# WSL/native infra 引导（Windows 开发机）
npm run dev:infra:setup
```

### 数据库

```bash
npm run db:validate      # Prisma schema 校验
npm run db:migrate       # 本地迁移（开发）
npm run db:deploy        # 生产迁移
npm run db:seed          # 种子数据
```

### 开发运行

```bash
npm run dev              # Next.js :3000
npm run worker:rpa       # RPA Worker（另开终端）
npm run worker:scheduler   # 超时清扫 Worker
npm run dev:stop         # 停止本地进程
npm run dev:clean        # 清理临时产物
```

### 测试与门禁

```bash
npm run lint             # TypeScript 类型检查
npm run test:unit        # 单元测试
npm run test:integration # 集成测试
npm run test:e2e         # Playwright E2E
npm run test             # 全量 Vitest

npm run smoke:connect    # MySQL/Redis 连通
npm run smoke:api        # API 冒烟
npm run smoke:rpa        # 真实 RPA 冒烟（需 env 配齐）

# RPA 探针（Go/No-Go 门禁：连续 3 次 exit 0）
npm run probe:rpa
npm run probe:rpa:3x
npm run probe:human-doc-system   # 人工 docx 10 组，系统 API 全链路
npm run probe:random-address-quote
npm run go-no-go         # 上线前指标检查
```

### RPA 录制与排障

```bash
# P0 真人完整链路录证（HAR+录像+Trace+Console+Network，定位 place commit 瞬间）
npm run record:human-commit-baseline
# 详见 docs/真人录证-commit时刻.md

# headed 录制 Mothership 报价流程 → 更新 MOTHERSHIP_QUOTE_URLS / RPA_SELECTOR_*
npm run record:mothership

# P0 真人 vs RPA 网络差分
$env:RPA_HEADLESS="false"
npm run compare:human-rpa-network
```

### Docker 全栈

**方式 1 — 全 Docker（MySQL 在容器内）**：见 [`deploy/README.md`](../deploy/README.md)

```bash
cp deploy/.env.production.example .env   # 编辑必填项
bash deploy/install.sh
bash deploy/install.sh --nginx-http    # 可选：Nginx 反代
```

**方式 2 — 宝塔宿主机 MySQL + Docker 应用（内网推荐）**：见 [`deploy/宝塔部署说明.md`](../deploy/宝塔部署说明.md)

```bash
# 代码目录示例：/home/project/chajia
cp deploy/.env.bt-host-mysql.example .env   # 编辑 DATABASE_URL、HOST_SERVICE_TOKENS 等
bash deploy/install-bt-host-mysql.sh
# 访问 http://<内网IP>:30325
```

上传更新：`deploy/pack-server-upload.ps1` 生成 `上传到服务器/代码/`；**勿覆盖服务器已有 `.env`**。

**本地开发 Compose**：

```bash
docker compose up -d --build
docker compose exec next-app npx prisma migrate deploy
docker compose exec next-app npx prisma db seed
```

### 访问地址

| 入口 | URL |
|------|-----|
| 宿主内嵌演示 | http://localhost:3000/embed-demo |
| 管理端预警 | http://localhost:3000/admin/alerts |
| 管理端 RPA | http://localhost:3000/admin/rpa |
| 队列监控 | http://localhost:3000/admin/queues |

---

## 编码规范

### 分层（禁止跨层）

```
app/api/*          → 薄路由，只调 modules
modules/*          → 业务编排（orchestrator、pricing、cache）
workers/rpa/*      → 长任务 RPA，不经 HTTP 直调 DB 以外层
lib/*              → 基础设施封装
components/*       → UI，经 hooks/api 调后端
```

- API Route **不得**写复杂业务逻辑
- RPA Worker **不得**绕过 `QuoteProvider` 接口直接返回假数据（CI 除外 `RPA_MOCK_MODE=true`）

### 命名

| 范围 | 规范 | 示例 |
|------|------|------|
| DB 字段 | snake_case | `pallet_count`, `raw_freight` |
| TS 变量/函数 | camelCase | `cargoHash`, `getQuote` |
| TS 类型/组件 | PascalCase | `QuoteRecord`, `QuoteForm` |
| 环境变量 | UPPER_SNAKE | `MOTHERSHIP_QUOTE_URLS` |
| API 路径 | kebab/REST | `/api/quotes/{quote_id}` |

### 金额与精度

- 存储与计算：`DECIMAL(12,2)`
- 舍入：`ROUND_HALF_UP`，保留 2 位
- **禁止**用 `float` / `number` 做金额运算
- **单位换算**：内部 `kgToLb` / `cmToIn` 保留 2 位（GD-13）；调用 MotherShip `axel/quote` 前须 `toAxelWholePounds` / `toAxelWholeInches` 向上取整

### API 响应

统一包络：

```json
{ "code": 0, "message": "ok", "data": { } }
```

错误使用标准 `error_code`（见 PRD §5.6 / `domain-rules.mdc` §10）。

### 异步与错误

- 所有 `async` 必须 `catch` 并 `log`（禁止空 catch）
- 用户可见操作必须有 loading + 防重复提交 + 成功/失败反馈
- 外部调用（RPA/Redis/MySQL）须处理超时与失败分类

### 前端

- **界面文案一律中文**（除 Mothership 原文抓取字段）
- 查价表单须支持地址联想点选、`pallet_count`、kg/cm 展示与 lb/in 换算
- 轮询：`GET /quotes/{id}`，间隔 2s，最长 30s

### Git

- 禁止在 `main` 直接开发；使用 feature 分支
- Commit：`feat|fix|docs|refactor(module): 描述`（可附 PRD/任务编号）
- 不提交 `.env`、`.rpa/` 敏感状态（除示例模板）

### 变更流程

1. 先查 PRD / 技术设计 / `implementation-plan.mdc` 是否已有规则
2. 数据结构变更 → Prisma migration → 再改代码
3. 改逻辑 → 补/跑对应测试
4. 完成一步 → 在 `implementation-plan.mdc` 标记 `Done` + 两行执行记录

---

## 红线（绝对不能违反）

### 业务红线

| # | 规则 | 违反后果 |
|---|------|----------|
| R1 | **一次询价 = 1 次 RPA**，单次必须返回 **4 档** | 成本翻倍、验收失败 |
| R2 | `service_level` / `rate_option` **不得进入** cache key | 缓存串档、价格错误 |
| R3 | 地址必须联想**点选**，须 `place_id` + `selected_from_suggestions=true` | Mothership 拒单或 RPA 失败 |
| R4 | 使用 `pallet_count`（托盘数），**禁止**件数/箱数字段 | 与 Mothership 口径不一致 |
| R5 | 加价仅对 `raw_freight`，上限 30%；未配置客户 0% | 财务口径错误 |
| R6 | L3 stale **仅** RPA 失败降级只读，正常路径不得读 L3 | 展示过期价 |
| R7 | `MOTHERSHIP_QUOTE_URLS` 必填且 headed 录制验证；**禁止**默认 `dashboard.mothership.com` 或 `/quote` | RPA 100% 失败 |
| R8 | login 页且无凭据 → `STRUCT_CHANGE`，**禁止**误报 `SESSION_EXPIRED` | 排障误导、错误重试 |
| R9 | RPA selector **禁止**硬编码上线；必须 `RPA_SELECTOR_*` env 或 `lib/rpa/selector-specs.ts` 统一管理 | 页面改版即挂 |
| R10 | `RPA_MOCK_MODE=false` 为验收/生产默认；Mock 仅 CI/单测 | 假数据上线 |

### 工程红线

| # | 规则 |
|---|------|
| E1 | PRD 与代码冲突时 **以 PRD 为准**；实现细节以技术设计为准 |
| E2 | 禁止擅自新增/修改 API 字段、错误码、表字段（须先改 PRD + migration） |
| E3 | 禁止空 catch、静默失败、吞异常 |
| E4 | 禁止占位 UI / 假按钮 / 「敬请期待」入口 |
| E5 | 禁止物理删除业务数据；使用逻辑删除 `is_deleted` |
| E6 | 禁止跳过 migration 直接改库；禁止代码引用未迁移字段 |
| E7 | 禁止在 main 分支直接提交；禁止 `git push -f` 到 main |
| E8 | 禁止提交密钥（`.env`、storageState、真实 token） |
| E9 | 禁止未授权升级依赖或引入未评审大包 |
| E10 | 多租户：`customer_id` 必须校验归属，禁止越权查历史/报价 |

### 安全红线

| # | 规则 |
|---|------|
| S1 | 查价 API 必须校验宿主 Service Token |
| S2 | 管理端 API 必须 JWT + RBAC |
| S3 | 高危操作须审计日志（`AuditLog`） |
| S4 | 限流：单客户 60 次/分钟（见 PRD） |

---

## 技术方案

### 架构总览

```
宿主系统（内嵌查价 UI）
    │ Bearer Token + customer_id
    ▼
Next.js API（QuoteOrchestrator）
    ├─ L1 idem:{request_id}  ──命中──► 直接返回
    ├─ L2 quote:{cargo_hash}   ──命中──► 加价后返回
    └─ 未命中 ──► BullMQ(quote-rpa) ──► RPA Worker
                        │                    │
                        │                    ├─ QuotePageAdapter（多 URL 探测）
                        │                    ├─ session-manager（storageState）
                        │                    └─ mothership.ts（填表+四档抓取）
                        ▼
                   MySQL quote_record / Redis L2/L3
```

### 缓存策略（GD-2）

| 层 | Key | TTL | 用途 |
|----|-----|-----|------|
| L1 | `idem:{request_id}` | 24h | 幂等完整响应 |
| L2 | `quote:{cargo_hash}` | 3min | 四档原始价（未加价） |
| L3 | `stale:{cargo_hash}` | 30min | RPA 失败降级 |

读取顺序：**L1 → L2 → RPA →（失败）L3**。

### 询价状态机

```
processing → done (cache|rpa|stale) | failed (QUOTE_UNAVAILABLE|QUOTE_TIMEOUT)
done --3min--> expired
```

### RPA 方案（v1.2 + 生产直连）

1. `QuotePageAdapter` 按 `MOTHERSHIP_QUOTE_URLS` 顺序探测可用入口
2. `session-manager` 加载/写回 `RPA_STORAGE_STATE_PATH`；`ensureMothershipStorageState` 可自动 bootstrap
3. **报价主路径（生产）**：`RPA_QUOTE_MODE=direct` → node `fetchAxelQuoteItems`（axel HTTP）
4. **兜底（开发/可关）**：Direct 失败后 Widget 浏览器填表；生产设 `RPA_DISABLE_WIDGET_QUOTE_FALLBACK=true`
5. 地址：RPA 枚举候选 → 用户确认 → placeId commit
6. 失败分类：入口错误 / 地址未锁定 / 验证码 / 结构变更 / 数据无效 / axel 400（整数校验）

上线门禁：`probe-rpa` **连续 3 次 exit 0**（TC-601 + §12.7）；系统链路 `probe:human-doc-system` 10/10。

### 加价公式

```
markup_amount = ROUND_HALF_UP(raw_freight × markup_percent / 100, 2)
final_total   = ROUND_HALF_UP(raw_total + markup_amount, 2)
```

### 关键 API

| 方法 | 路径 | 调用方 |
|------|------|--------|
| POST | `/api/host/quote/candidates` | 宿主（方式 A，免 Token） |
| POST | `/api/host/quote/submit` | 宿主（方式 A，同步返回报价） |
| POST | `/api/quotes` | 宿主（方式 B） |
| GET | `/api/quotes/{quote_id}` | 宿主（方式 B 轮询） |
| POST | `/api/addresses/mothership-candidates` | 宿主（方式 B 地址候选） |
| GET | `/api/quotes/history` | 宿主 |
| PUT | `/api/markup-configs/{customer_id}` | 宿主 |
| GET | `/api/alerts` | 管理员 |

### 环境变量（RPA 最小集）

| 变量 | 必填 | 说明 |
|------|------|------|
| `MOTHERSHIP_QUOTE_URLS` | 是 | 逗号分隔匿名报价 URL |
| `RPA_SELECTOR_*` | 是 | 录制产出 selector |
| `RPA_STORAGE_STATE_PATH` | 否 | 默认 `.rpa/mothership-storage.json` |
| `RPA_MOCK_MODE` | 否 | 默认 `false` |
| `RPA_HEADLESS` | 否 | 调试设 `false` |
| `RPA_QUOTE_MODE` | 否 | 生产 `direct` |
| `RPA_DISABLE_WIDGET_QUOTE_FALLBACK` | 否 | 生产建议 `true` |
| `HOST_PUBLIC_API_ENABLED` | 否 | 宿主方式 A 开关 |
| `DATABASE_URL` / `REDIS_URL` | 是 | 基础设施 |

完整列表见 `.env.example` 与技术设计 §6.2。

---

## 新人上手路径

1. **先读本文「AI 项目交接文档」** → PRD §1–3 → 技术设计 §1、§7
2. `cp .env.example .env`，配 `DATABASE_URL` / `REDIS_URL` / `HOST_SERVICE_TOKENS`
3. `npm run dev:start` 或 `docker compose up -d` + `npm run dev`
4. 打开 http://localhost:3000/embed-demo 走通查价
5. RPA 开发：先 `npm run record:mothership`，再 `npm run probe:rpa`
6. 接任务前查 [工程任务拆解 v1.2](./查价系统-工程任务拆解-v1.2.md) 当前 Phase

---

## 排障速查

| 现象 | 优先检查 |
|------|----------|
| **axel/quote HTTP 400 整数** | 公制货物是否经 `toAxelWhole*`；worker 日志 `Weight/Length must be a whole number` |
| **GET /quotes/{id} 返回 HTML** | 动态路由是否打包进镜像；`deploy/pack-server-upload.ps1` 预检 |
| **embed 询价超时/failed** | **先看「AI 项目交接文档」§3 BLK-001~003**；worker 日志 `no_place_request` |
| **直连失败且极慢** | 是否未关 Widget fallback；查 `RPA_DISABLE_WIDGET_QUOTE_FALLBACK` |
| **Service Token 无效** | token 是否在 `HOST_SERVICE_TOKENS`；embed-demo 与 `NEXT_PUBLIC_EMBED_DEMO_*` 一致 |
| 创建即 done + cache | L2 假阳性；换 cargo 或清 Redis `quote:*` |
| place commit 失败 | headed 对比 network：`GET place/{id}` 是否触发 |
| RPA 进登录页 | `MOTHERSHIP_QUOTE_URLS` 是否 dashboard；storageState 是否失效 |
| preCheck 失败 | `RPA_SELECTOR_*` 是否过期；headed 重录 |
| 地址填不进去 | 是否缺少 `place_id`；联想是否 click 而非 Enter |
| 只返回 2 档 | 违反 GD-1，检查四档抓取逻辑 |
| 429 | 限流；检查 `customer_id` 请求频率 |
| Worker 不消费 | Redis 连通、`RPA_QUEUE_ENABLED`、BullMQ 队列 |

失败截图目录：`.dev/logs/`（如 `rpa-failure-*.png`）。

---

## 文档维护

- 业务变更 → 先改 PRD → 同步技术设计 → 更新本文「项目信息/红线/技术方案」摘要
- 命令变更 → 以 `package.json` `scripts` 为准，同步本文「常用命令」
- 规则变更 → 同步 `cursor_project_rules/domain-rules.mdc`
