36 KiB
查价项目
新人入口文档: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-system10/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(生产模板);nodefetchAxelQuoteItems为主路径。 - 原因:Widget 浏览器填表回退单次 6~12 分钟且 worker 并发为 1,易阻塞队列。
- 权衡:直连对 payload 更严格(整数磅/英寸、有效 placeId);失败须快速报错而非长时间挂起。
ADR-7:内部精度与 axel API 边界分离
- 决策:
kgToLb/cmToIn内部保留 2 位小数(GD-13 哈希/展示);调用 MotherShipaxel/quote前toAxelWholePounds/toAxelWholeInches向上取整。 - 原因:embed-demo 默认英制整数不易暴露;公制 API / 宿主对接会触发小数。
- 权衡:取整仅影响 MotherShip 边界,不改变 L2 cache key 算法(仍用内部精度)。
5. 新 AI 接手指令 (Onboarding Instructions)
启动/运行命令
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 |
首要任务优先级
- P0 回归门禁:
npm run probe:human-doc-system10/10(改地址/RPA 后必跑;worker 需dev:stop+dev:start重载) - P0 实网验收:
npm run prove:rpa-chain在RPA_ADDRESS_MODE=real下 PASS(非 cache、非 fixture) - P1 embed-demo 标准样例(LA 90001 → Dallas 75201,2 托 500lb)返回
source_type=rpa且价与 MotherShip 页面一致 - P2 M2 门禁:
npm run probe:rpa:3x连续 exit 0 - 禁止:在 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 v0.6 | 业务规则、GD 决策、验收标准(最高业务权威) |
| 查价系统-技术设计.md v1.2 | 架构、目录、API、RPA 规格(实现权威) |
| 查价系统-工程任务拆解-v1.2.md | 原子任务与开发顺序 |
| 查价系统-UI设计.md | 页面、交互、状态机 |
| 参考-Mothership-RPA-开源借鉴与解阻塞.md | RPA 排障参考 |
| deploy/README.md | 全 Docker 生产部署 |
| deploy/宝塔部署说明.md | 宝塔 MySQL + Docker 应用(内网 30325) |
| deploy/API.md | 外部系统接口速查 |
| deploy/第三方对接指南.md | 宿主对接完整指南(方式 A/B) |
| 报价提速与稳定性-改动清单.md | 提速/稳定性分批实施清单(Phase 12) |
| AI 项目交接文档 | 完整 handoff:身份/状态/阻塞/ADR/接手指令 |
cursor_project_rules/ |
AI/人工编码规则与实施日志 |
目录结构(关键路径)
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} 轮询 |
联调标准样例
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 |
常用命令
环境准备
# 复制环境变量(勿提交 .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
数据库
npm run db:validate # Prisma schema 校验
npm run db:migrate # 本地迁移(开发)
npm run db:deploy # 生产迁移
npm run db:seed # 种子数据
开发运行
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 # 清理临时产物
测试与门禁
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 录制与排障
# 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
cp deploy/.env.production.example .env # 编辑必填项
bash deploy/install.sh
bash deploy/install.sh --nginx-http # 可选:Nginx 反代
方式 2 — 宝塔宿主机 MySQL + Docker 应用(内网推荐):见 deploy/宝塔部署说明.md
# 代码目录示例:/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:
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);调用 MotherShipaxel/quote前须toAxelWholePounds/toAxelWholeInches向上取整
API 响应
统一包络:
{ "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/敏感状态(除示例模板)
变更流程
- 先查 PRD / 技术设计 /
implementation-plan.mdc是否已有规则 - 数据结构变更 → Prisma migration → 再改代码
- 改逻辑 → 补/跑对应测试
- 完成一步 → 在
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 + 生产直连)
QuotePageAdapter按MOTHERSHIP_QUOTE_URLS顺序探测可用入口session-manager加载/写回RPA_STORAGE_STATE_PATH;ensureMothershipStorageState可自动 bootstrap- 报价主路径(生产):
RPA_QUOTE_MODE=direct→ nodefetchAxelQuoteItems(axel HTTP) - 兜底(开发/可关):Direct 失败后 Widget 浏览器填表;生产设
RPA_DISABLE_WIDGET_QUOTE_FALLBACK=true - 地址:RPA 枚举候选 → 用户确认 → placeId commit
- 失败分类:入口错误 / 地址未锁定 / 验证码 / 结构变更 / 数据无效 / 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。
新人上手路径
- 先读本文「AI 项目交接文档」 → PRD §1–3 → 技术设计 §1、§7
cp .env.example .env,配DATABASE_URL/REDIS_URL/HOST_SERVICE_TOKENSnpm run dev:start或docker compose up -d+npm run dev- 打开 http://localhost:3000/embed-demo 走通查价
- RPA 开发:先
npm run record:mothership,再npm run probe:rpa - 接任务前查 工程任务拆解 v1.2 当前 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.jsonscripts为准,同步本文「常用命令」 - 规则变更 → 同步
cursor_project_rules/domain-rules.mdc