You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mail-yubao/docs/项目全貌与开发指南.md

785 lines
34 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 项目全貌与开发指南
> **文档定位**:`X:\work\yx`(包名 `email-forecast`)后续开发的唯一权威参考(系统核心知识库)。
> **产品名**:邮件自动预报系统
> **形态**:Next.js 运营 Web + IMAP Worker + MySQL,**不是**桌面助手、不是客服渠道。
> **代码根目录**:`X:\work\yx`
> **文档版本**:v1.0
> **编写日期**:2026-08-18
> **对照材料**:`README.md`、`docs/邮箱项目.md`、`docx/` PRD/技术设计、当前源码。
> **密钥纪律**:只写配置**键名与路径**,不写 IMAP 授权码、CC 密码、OAuth secret 的具体值。
与 `carriercentral-ai-assistant` 的关系:yx 是**运营后台**(拉邮件 → 解析 → 人工确认 → 写 CarrierCentral SaaS);助手仓是**桌面查询 Demo**(读仓)。两边打同一套海外仓 HTTP,但进程、UI、状态机完全独立。把 yx 能力迁到助手仓时,以本文模块名为地图,**禁止**把 Worker 的 SaveContainer 禁令或 IMAP 轮询直接塞进 Tkinter。
---
## 0. 阅读路径与权威优先级
材料冲突时,优先级从高到低:
1. **正在运行的代码**:`src/app/**`、`src/services/**`、`src/worker/index.ts`、`prisma/schema.prisma`
2. **行为契约**:`docx/需求规格-邮件自动预报-v0.2.md`、`docx/接口/carriercentral客户端通用接口V1.73.md`
3. **协作入口**:`docs/邮箱项目.md`(红线、排障、上市清单)
4. **指令规则**:`docx/产品规则-邮件指令识别与拆分.md`、`docs/邮件指令提取规则.md`
5. **速查**:`README.md`
改识别规则先改 `docx/产品规则-*` 再改 `src/services/parse/`。改 CC 路径以 V1.73 为准;文档外 endpoint 默认 mock。
---
## 1. 技术栈与全局依赖
### 1.1 系统形态
三进程,无独立消息队列、无 Redis、无 GraphQL。
| 组件 | 本仓库状态 |
|------|------------|
| 前端 | **Next.js 15 App Router** + **React 18** + **Ant Design 5** |
| 后端 | 同进程:`src/app/api/**/route.ts`(Route Handlers) |
| Worker | 独立进程 `src/worker/index.ts`(`tsx` 跑源码) |
| ORM | **Prisma 5** + **mysql2** |
| 数据库 | **MySQL 8**(库名 `email_forecast`,宿主机端口 **7023**) |
| 缓存 | **无 Redis**。CC token 在表 `cc_token_cache`;会话在 Cookie |
| 消息队列 | **无**。补偿/自动执行靠 Worker 轮询 `import_compensation.next_retry_at` |
| 状态管理 | **无 Redux/Pinia**。客户端 `useState` + hooks(`useMails` / `useMailDetail`)+ `AuthSessionProvider` |
| 包管理 | **仅 pnpm**(不要 npm/yarn) |
### 1.2 关键三方库(`package.json`)
| 包 | 用途 |
|----|------|
| `next` ^15.1.3 | App Router、API、生产 `next start -p 3100` |
| `react` / `react-dom` 18 | UI |
| `antd` ^5.22 + `@ant-design/icons` + `cssinjs` + React19 patch | 运营后台组件库 |
| `@prisma/client` / `prisma` | schema → MySQL |
| `mysql2` | Prisma 驱动;`db-lock.ts` 独立连接做 `GET_LOCK` |
| `iron-session` | Cookie 会话 `email_forecast_session` |
| `imapflow` | IMAP 拉取 / IDLE |
| `mailparser` | 解析 eml |
| `exceljs` | 装箱清单 / 转仓表 xlsx |
| `adm-zip` | zip 解一层 |
| `tesseract.js` | `OCR_PROVIDER=local` 启发式 OCR |
| `undici` | CC HTTP(`src/services/cc/http.ts`) |
| `zod` | env 与 API 入参校验 |
| `pino` | 结构化日志 `src/lib/logger.ts` |
| `uuid` | 幂等 key、补偿 `retry_token`、trace |
| `@tanstack/react-virtual` | 确认页大表 `ShipmentVirtualTable` |
开发:`vitest`、`@playwright/test`、`tsx`、`typescript`。Lint:`next lint`(无独立 ESLint/Prettier 配置文件)。
### 1.3 本仓库明确没有的技术
- Vue / 桌面 Tkinter / LangChain Agent
- Redis / Kafka / RabbitMQ
- 独立 Tenant 表 / 一租户多登录用户(当前为一租户一 `AppUser.role=customer` 账号)
- SMTP 自动回信、真实大模型指令识别(词表/规则)
---
## 2. 目录结构详解
组织逻辑:**按运行时分层 + 按业务域分子目录**。
```
页面/API(src/app)
→ hooks / components(UI)
→ services(imap / parse / import / cc)
→ lib(env / session / prisma 辅助)
→ prisma + MySQL
Worker(src/worker)直接调同一套 services,禁止走 ConfirmImport 写新增预报。
```
### 2.1 仓库根
| 路径 | 职责 |
|------|------|
| `package.json` | 脚本与依赖;`name: email-forecast` |
| `pnpm-lock.yaml` | 锁文件 |
| `docker-compose.yml` | `mysql` + `web` + `worker` |
| `Dockerfile` | 多阶段 Node 20 bookworm;web 入口 `docker-entrypoint-web.sh` |
| `next.config.ts` | Antd transpile、关 dev 指示标、client 禁 `async_hooks` |
| `tsconfig.json` | `@/*` → `./src/*`,strict |
| `tsconfig.worker.json` | Worker 编译(compose 实际用 tsx 跑源码) |
| `vitest.config.ts` | 单测 + coverage |
| `playwright.config.ts` | e2e,baseURL `3100` |
| `playwright.test-mode.config.ts` | TEST_MODE 端口 3110 |
| `.env.example` | 环境变量模板 |
| `prisma/schema.prisma` | 全表 |
| `prisma/seed.ts` | 仅账号 + 部分 write capability |
| `scripts/` | 启动栈、IMAP/CC 烟雾、retention、样例导入 |
| `fixtures/emails/` | 01–16 金样例(spec + expected) |
| `docx/` | PRD / 技术设计 / UI / 韧性 / 金样例 JSON / CC 接口 |
| `docs/` | 协作文档 + 本知识库 + Windows 一键脚本 |
| `data/` | 运行时 eml/附件/日志(gitignore);compose 挂载 `./data` |
| `tests/unit` `tests/integration` `tests/e2e` | 测试 |
### 2.2 `src/app/` — 页面与 API
| 路径 | 职责 |
|------|------|
| `layout.tsx` | 根:`lang=zh-CN`、`AntdProvider`、标题「邮件自动预报」 |
| `globals.css` | 全局样式 |
| `error.tsx` | 错误页 |
| `page.tsx` | `/` → `redirect("/mails")` |
| `(auth)/login/page.tsx` | 登录 |
| `(ops)/layout.tsx` | `AuthGuard` + `TopNav` + `main.ops-main` |
| `(ops)/mails/page.tsx` | 邮件列表 |
| `(ops)/mails/[id]/page.tsx` | 详情 |
| `(ops)/mails/[id]/confirm/page.tsx` | 确认导入(按 instruction 分发视图) |
| `(ops)/logs/page.tsx` | 导入日志 / 拉取记录 |
| `(ops)/settings/page.tsx` | Admin 设置 |
| `(ops)/test-workbench/page.tsx` | `TEST_MODE=1` 测试台 |
| `api/**/route.ts` | 42 个 Route Handler(见第 6 节) |
**无** `middleware.ts`。鉴权在各 route 的 `requireSession` / `requireAdmin`,页面靠客户端 `AuthSessionProvider`。
### 2.3 `src/services/` — 业务核
| 目录 | 职责 |
|------|------|
| `imap/` | `poller.ts` 轮询;`idle-supervisor.ts` IDLE;`snapshot.ts` 落 eml;`pull-filter.ts` 过滤;`mailbox-config.ts` 多箱;`stale-reaper.ts` 超时回收;`runtime-status.ts` 连续失败 |
| `parse/` | `pipeline.ts` 主解析;`classify.ts` 类型;`mail-intent.ts` 意图文案;`packing-list.ts` 装箱表;`split-instructions.ts` 指令拆分;`extract-container-header.ts` 柜头 |
| `import/` | `confirm.ts` **ConfirmImport**(唯一新增预报 SaveContainer);`compensation.ts` 补偿;`auto-exec.ts` 指令自动写;`confirm-do-upload.ts` / `confirm-batch-transfer.ts` / `confirm-work-order.ts` |
| `cc/` | `http.ts` `auth.ts` `save-container.ts` `do-upload.ts` `conflict.ts` `shipping-line.ts` `mock-*` `write-capability.ts` `settings-config.ts` |
| `ocr/` | `index.ts` local/aliyun/off;`sanitize.ts` |
| `oauth/` | `providers.ts` Google/MS;state HMAC |
| `customer/` | `resolve-sender.ts` 发件人→客户编码 |
| `alert/` | `webhook.ts` 企微/飞书 JSON POST |
| `retention/` | `cleanup.ts` |
| `test/` | TEST_MODE 工作台、fixture 评测 |
| `db.ts` | Prisma 单例 |
| `db-lock.ts` | **独立 mysql2 连接** `GET_LOCK` / `RELEASE_LOCK` |
| `state-machine.ts` | `assertTransition` / `resolveAfterParse` |
| `audit.ts` | 审计日志 |
### 2.4 `src/lib/` `src/components/` `src/hooks/` `src/constants/` `src/types/`
| 路径 | 职责 |
|------|------|
| `lib/env.ts` | Zod 校验环境;生产弱密钥拒绝启动 |
| `lib/session.ts` | iron-session;`verifyCredentials` **读 .env 账号,不读 `app_user` 表** |
| `lib/api.ts` | `ok`/`fail`、`requireSession`、`requireAdmin`、BigInt JSON |
| `lib/safe-path.ts` | 附件/快照必须在 `data/` 下 |
| `lib/cc-api-base.ts` | CC `api_base` 主机白名单(防 SSRF) |
| `lib/rate-limit.ts` | 进程内滑动窗口 |
| `lib/secret-crypto.ts` | AES-GCM,密钥派生自 `SESSION_SECRET` |
| `lib/logger.ts` | pino |
| `lib/mail-trace.ts` | 单封 `traceId` |
| `lib/test-mode.ts` / `test-mode-guard.ts` | TEST_MODE;测试 API 非测试环境返回 404 |
| `components/TopNav.tsx` | 顶栏:邮件 / 日志 / 设置(admin) / 测试台 |
| `components/MailTable.tsx` | 列表 |
| `components/confirm/*` | 确认页:预报 / DO / 转仓 / 工单 |
| `components/Cc*.tsx` | 对照 CC 表单的运营控件 |
| `hooks/useMails.ts` `useMailDetail.ts` `useImport.ts` `useCcUiMode.ts` | 客户端数据 |
| `constants/ui-copy.ts` `error-copy.ts` | 中文文案 |
| `types/mail.ts` | `MailType` / `MailStatus` / 柜头货件字段 |
### 2.5 `src/worker/index.ts`
常驻循环 `scheduleLoop`:
1. `ImapPoller.tick()`
2. `staleReaper.run()`(`PARSING`/`IMPORTING` 超时打回)
3. `drainFetchedMails(10)` 消化 `FETCHED`
4. `processDueAutoExec(10)`
5. `processDueCompensations(10)`
启动:`ImapPoller.clearStaleLock()`、`ensureCcWriteCapabilities()`、`IdleSupervisor.start()`。
吞掉 IMAP `ETIMEOUT` 的 `uncaughtException`,避免整进程退出。
---
## 3. 入口与启动流程
### 3.1 入口一览
| 入口 | 实质 |
|------|------|
| `pnpm dev` / `dev:stack` | `scripts/dev-stack.mjs`:同时 `next dev -p 3100` + `tsx src/worker/index.ts` |
| `pnpm dev:web` | 仅 Web,**不拉信** |
| `pnpm worker` | 仅 Worker |
| `pnpm start` | 生产 Next `:3100` |
| `.\docs\start-system.ps1` | Windows:mysql 容器 + 本机 web/worker(默认不拉 node 镜像) |
| `pnpm compose:up` | docker 三件套 |
| `prisma/seed.ts` | 账号种子 |
无 FastAPI `app.include_router`。Next 按文件系统挂载路由。
### 3.2 Web 启动全过程
```
pnpm dev:web
→ Next 加载 next.config.ts
→ 首次请求触发 getEnv()(读 process.env,Zod)
→ prisma 单例连 DATABASE_URL
→ App Router:
/ → redirect /mails
(ops)/* → 客户端 AuthSessionProvider 调 GET /api/auth/me
未登录 → /login
→ API:各 route 内 requireSession / requireAdmin
```
**没有**全局中间件链。登录:`POST /api/auth/login` → `verifyCredentials`(env)→ `session.save()`。
Docker web:`scripts/docker-entrypoint-web.sh`(`SEED_ON_START` 时可 db push + seed)→ `pnpm start`。compose 把 `DATABASE_URL` 改成 `mysql://app:app@mysql:3306/email_forecast`。
注意:`Dockerfile` `EXPOSE 3000`,实际监听 **3100**(`package.json` `start`)。
### 3.3 Worker 启动全过程
```
tsx src/worker/index.ts
→ getEnv()
→ clearStaleLock(imap_poll)
→ ensureCcWriteCapabilities() upsert 写能力行
→ IdleSupervisor.start() 各箱 IDLE(IMAP_IDLE_ENABLED 且账号 idle_enabled)
→ scheduleLoop:
tick → 按 imap_settings.pollIntervalMs(否则 POLL_INTERVAL_MS)+ 连续失败退避
```
数据库:compose `mysql` healthcheck 通过后 web/worker 才起。本地需先 `docker compose up -d mysql` + `pnpm exec prisma db push`。
---
## 4. 核心模块与依赖关系
### 4.1 模块划分
| 模块 | 关键文件 | 职责 |
|------|----------|------|
| 认证 | `lib/session.ts` `api/auth/*` | admin/ops Cookie |
| 邮箱绑定 | `imap/mailbox-config.ts` `api/settings/mailboxes*` `oauth/*` | 多箱、授权码/OAuth |
| IMAP 拉取 | `imap/poller.ts` `pull-filter.ts` `snapshot.ts` | 入库 `mail_message` |
| 解析 | `parse/pipeline.ts` `classify.ts` | 类型 + 柜头 + 货件 + 指令 |
| 状态机 | `state-machine.ts` | 合法迁移 |
| 确认导入 | `import/confirm.ts` | 预报 SaveContainer(仅 web) |
| 指令写 CC | `confirm-do-upload` / `confirm-batch-transfer` / `confirm-work-order` / `auto-exec` | DO/转仓/工单 |
| CC HTTP | `cc/http.ts` `auth.ts` | customerLogin + 写接口 |
| 冲突锁 | `cc/conflict.ts` `container_active_lock` | 同柜互斥 |
| 补偿 | `import/compensation.ts` | TIMEOUT_UNKNOWN 重试 ≤3 |
| 设置 | `api/settings/*` | IMAP 间隔、过滤、CC、OAuth |
| 可观测 | `api/health` `alert/webhook.ts` `imap-pull-log` | 健康与告警 |
| TEST | `api/test/*` `TestWorkbench` | 仅 TEST_MODE |
调用方向(禁止反向):
```
页面 → hooks/client-api → Route Handler → services.* → prisma / CC / IMAP
Worker → services.imap|parse|import.compensation|auto-exec
Worker ──X── ConfirmImport.execute(新增预报)
```
### 4.2 公共层
| 能力 | 文件 |
|------|------|
| API 信封 `{ok, data}` / `{ok:false, error}` | `lib/api.ts` |
| 限流 | `lib/rate-limit.ts` |
| 命名锁 | `db-lock.ts`(禁止 Prisma 池直接 GET_LOCK) |
| 路径安全 | `safe-path.ts` |
| 审计 | `audit.ts` |
| 中文错误 | `constants/error-copy.ts` `formatOperatorError` |
### 4.3 页面层级与组件树
```
RootLayout (AntdProvider)
/login 无 TopNav
OpsLayout (AuthGuard + TopNav)
/mails MailTable + 筛选
/mails/[id] 详情:摘要、附件、重新解析、改类型(admin)
/mails/[id]/confirm 按 instruction_id:
ForecastConfirmView
DoUploadConfirmView
TransferConfirmView
WorkOrderConfirmView
InstructionConfirmPicker
/logs 导入 + ImapPullLogsPanel
/settings Admin:ImapPullSettingsCard、邮箱、CcSettingsCard、OauthSettingsCard
/test-workbench TestWorkbench(TEST_MODE)
```
顶栏 `TopNav`:`ImapStatusBanner` + 菜单 + 角色 Tag + 登出。
确认页 `confirm/page.tsx`:`extractInstructionsFromMail` → `resolveConfirmInstruction`;无 `instruction_id` 时 `router.replace(confirmHref(...))`。
---
## 5. 数据模型与数据流
### 5.1 表结构(`prisma/schema.prisma` → MySQL)
```
mailbox_account 1──* mail_message 1──* mail_attachment
│
├──1 parse_result(柜头/货件 JSON)
└──* container_import 1──* import_compensation
container_active_lock(柜号主键,跨邮件互斥)
cc_token_cache / cc_settings / cc_write_capability / oauth_settings
imap_settings / imap_filter_rule / imap_pull_log
sender_customer_map
app_user ← seed 写入;登录当前不查此表
```
**MailboxAccount** `mailbox_account`:IMAP 主机/端口/用户;`password_enc` AES-GCM;`auth_type` `PASSWORD|OAUTH`;OAuth 令牌密文与过期;`idle_enabled`;`username` 唯一。
**MailMessage** `mail_message`:幂等 `message_id`、`(mailbox, folder, imap_uid)`、`raw_hash`;`mail_type`;`status`;`type_evidence` JSON;`snapshot_path`;`trace_id`;`version`(乐观锁,导入用);正文/OCR/线程字段。
**MailAttachment**:`sha256`、`path`(相对 `data/`)、`rejected`、`template_id`。
**ParseResult**:`container_header` / `shipments` / `lineage` JSON,与 `types/mail.ts` 的 `ContainerHeader`、`ShipmentRow` 对齐。
**ContainerImport**:每次向 CC 提交一柜;`shipments_hash` 参与唯一键 `uk_mail_container_shiphash`;`external_id` 为 CC 返回 id。
**ImportCompensation**:`reason`、`retry_count`/`max_retry`、`next_retry_at`、`retry_token` UUID。
**CcSettings** id=1:mock、api_base、saas、login_mark、username、`password_enc`。优先于 `.env`。
**CcWriteCapability**:`save_container` / `do_upload` / `transfer` / `batch_transfer` / `hold_split` / `label` / `customer_message`,mode `live|mock|off`。
**ImapSettings** id=1:`poll_interval_ms` 默认 1800000(30min),合法 3min~7d。
**ImapFilterRule**:`kind` + `value`(关键词/白名单/黑名单等)。
**ImapPullLog**:每封拉取结果,最多约 1000 条。
**AppUser**:username + sha256 `password_hash` + role。**现行登录走 env,此表不参与鉴权**(隐式债务)。
### 5.2 邮件状态机(`state-machine.ts`)
| from | 允许 to |
|------|---------|
| FETCHED | PARSING, IGNORED |
| PARSING | PENDING_CONFIRM, PARSED, PARSE_FAILED, REJECTED_VALIDATION, AUTO_EXECUTING, FETCHED |
| PARSED | PARSING, PENDING_CONFIRM, AUTO_EXECUTING, IGNORED |
| PENDING_CONFIRM | IMPORTING, PARSING, IGNORED, PENDING_CONFIRM |
| IMPORTING | SUCCESS, PARTIAL_SUCCESS, FAILED |
| AUTO_EXECUTING | SUCCESS, FAILED, PARSED |
| SUCCESS | (终态) |
| PARTIAL_SUCCESS | IMPORTING |
| FAILED | IMPORTING, PENDING_CONFIRM, PARSING, AUTO_EXECUTING, IGNORED |
| PARSE_FAILED | PARSING, IGNORED |
| REJECTED_VALIDATION | PARSING, IGNORED, PENDING_CONFIRM |
| IGNORED | (终态) |
非法迁移抛 `IllegalTransition`。`IMPORTING`/`SUCCESS`/`PARTIAL_SUCCESS` 禁止重新解析。
`resolveAfterParse`:`NEW_CONTAINER` 且有效货件≥1 → `PENDING_CONFIRM`;`WORK_ORDER`/`DO_UPLOAD`/`TRANSFER` → `PENDING_CONFIRM`;`autoExecEligible` → `AUTO_EXECUTING`;否则 `PARSED`。
### 5.3 邮件类型 `MailType`(`types/mail.ts`)
`NEW_CONTAINER` | `TRANSFER` | `DO_UPLOAD` | `INSTRUCTION_HOLD_SPLIT` | `INSTRUCTION_LABEL` | `WORK_ORDER` | `UNKNOWN`。
分类在 `classify.ts`(主题/正文/附件名打分),不是 LLM。
### 5.4 主数据流
```
IMAP UID
→ pull-filter(黑名单拒绝 → 白名单必拉 → 关键词;皆空则 isBusinessRelevantMail)
→ snapshot 落 data/ + Prisma upsert(幂等)
→ 成功后 IMAP \Seen(失败且未入库则不标已读)
→ status FETCHED
→ ParsePipeline(Worker drain 或拉取后立即 parse)
→ parse_result + PENDING_CONFIRM / PARSED / ...
→ 运营打开 /mails/[id]/confirm
→ POST /api/mails/:id/import
ConfirmImport.execute
version 校验 → 柜冲突检查 → 状态 IMPORTING
→ saveContainer(CC 或 mock)
→ SUCCESS / 补偿 TIMEOUT_UNKNOWN
→ 浏览器列表刷新
```
前端:`useMails` → `GET /api/mails`;详情 `useMailDetail` → `GET /api/mails/:id`。无全局 store。
导入状态 `ImportStatus`:`PENDING|IMPORTING|SUCCESS|FAILED|CONFLICT|TIMEOUT_UNKNOWN`。
---
## 6. API 接口与页面路由
统一响应:`{ ok: true, data }` 或 `{ ok: false, error: { code, message, details? } }`。BigInt 转字符串。
权限:`public` / `session`(admin|ops)/ `admin`。`/api/test/*` 另需 `TEST_MODE` 否则 404。
### 6.1 认证与健康
| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| POST | `/api/auth/login` | public | 限流 10/分/IP+用户名;写 Cookie |
| POST | `/api/auth/logout` | Cookie | 销毁会话 |
| GET | `/api/auth/me` | session | 当前用户 |
| GET | `/api/health` | **public** | mysql、IMAP 告警、`cc_mode`/`cc_live_ready`、`test_mode`;勿塞密钥 |
### 6.2 邮件
| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| GET | `/api/mails` | session | 分页筛选 `mail_type`/`status`/`content_kind`/`q`/`sort` |
| DELETE | `/api/mails` | session | `ids` 或 `oldest` 批量删 |
| GET | `/api/mails/[id]` | session | 详情+解析+附件 |
| POST | `/api/mails/[id]/import` | session | ConfirmImport;10/分/用户;`force_skip_conflict` 仅 admin |
| POST | `/api/mails/[id]/reparse` | session | 重新解析 |
| POST | `/api/mails/[id]/type` | admin | `ENABLE_TYPE_OVERRIDE`;改 `mail_type` |
| POST | `/api/mails/[id]/ignore` | admin | → IGNORED |
| GET | `/api/mails/[id]/attachments/[attId]/download` | session | `resolveDataFile` 读盘 |
| POST | `/api/mails/[id]/ocr-attachments` | session | 触发附件 OCR |
| POST | `/api/mails/[id]/do/confirm` | session | DO 确认上传 |
| POST | `/api/mails/[id]/transfer/confirm` | session | 转仓确认 |
| GET | `/api/mails/[id]/transfer/gate` | session | 转仓门禁 |
| POST | `/api/mails/[id]/work-order/confirm` | session | 工单确认 |
### 6.3 导入 / 补偿 / 船司 / 拉取日志
| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| GET | `/api/imports` | session | 导入日志 |
| POST | `/api/imports/[importId]/retry` | session;部分 admin | 失败重试 |
| POST | `/api/compensations/[id]/retry` | session / admin 分支 | 补偿重试 |
| GET | `/api/shipping-lines` | session | CC `GetShippingLineList` |
| GET | `/api/imap-pull-logs` | session | 拉取记录 |
| POST | `/api/admin/imap/poll` | admin | 手动拉一轮,锁等待 20s |
### 6.4 设置(均 admin)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET/PUT | `/api/settings/imap` | 拉取间隔 |
| GET/POST | `/api/settings/imap/filters` | 过滤规则 |
| PATCH/DELETE | `/api/settings/imap/filters/[id]` | 单条规则 |
| GET/POST | `/api/settings/mailboxes` | 邮箱列表/新增 |
| PATCH/DELETE | `/api/settings/mailboxes/[id]` | 改/删 |
| POST | `/api/settings/mailboxes/[id]/test` | 测连通 |
| POST | `/api/settings/mailboxes/probe` | 探测 |
| GET/PUT | `/api/settings/cc` | CC 连接(api_base 白名单) |
| POST | `/api/settings/cc/test` | CC 测试 |
| GET/PUT | `/api/settings/oauth` | OAuth 应用凭证 |
### 6.5 OAuth
| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| GET | `/api/oauth/providers` | (见源码) | 提供商是否配置 |
| GET/POST | `/api/oauth/start` | admin | 发起绑定,带 HMAC state |
| GET | `/api/oauth/callback/[provider]` | **无登录 Cookie** | 靠 state.actor;写 mailbox 后重定向 `/settings?oauth=` |
### 6.6 TEST_MODE(`requireTestMode`)
`/api/test/faults` `inject` `pipeline` `fixtures` `reset` `run-all` `target-requests`:注入样例、切故障、跑流水线。生产必须 `TEST_MODE=false`。
### 6.7 页面路由
| 路径 | 文件 | 说明 |
|------|------|------|
| `/` | `app/page.tsx` | 跳转 `/mails` |
| `/login` | `(auth)/login/page.tsx` | 登录 |
| `/mails` | `(ops)/mails/page.tsx` | 列表 |
| `/mails/[id]` | `(ops)/mails/[id]/page.tsx` | 详情 |
| `/mails/[id]/confirm` | `confirm/page.tsx` | 确认;query `instruction_id` |
| `/logs` | `(ops)/logs/page.tsx` | 日志 |
| `/settings` | `(ops)/settings/page.tsx` | Admin |
| `/test-workbench` | `(ops)/test-workbench/page.tsx` | 测试台 |
---
## 7. 关键业务流程
### 7.1 登录
1. `POST /api/auth/login` `{username, password}`
2. `checkSlidingWindow` → `verifyCredentials` 比对 `APP_ADMIN_*` / `APP_OPS_*`
3. iron-session 写入 `username`+`role`
4. 客户端进 `(ops)`,`AuthSessionProvider` 调 `/api/auth/me`
状态:未登录只能 `/login`;Cookie `HttpOnly`,生产 `secure`。
### 7.2 IMAP 拉取 → 解析(Worker)
```
Idle 或 定时 tick
→ withMysqlNamedLock("imap_poll")
→ resolveEnabledMailboxes(DB 优先,否则 .env IMAP_*)
→ SEARCH SINCE lookback(默认 3 天,已读+未读)
→ 每 UID:decidePull → 跳过则 stub + pull_log
→ FETCH + parseMailSource + saveMailSnapshot
→ upsert mail_message(P2002 幂等)
→ ParsePipeline / 或 FETCHED 待 drain
→ markSeen
```
关键函数:`ImapPoller.tick`、`pollMailbox`、`ParsePipeline`(`pipeline.ts`)、`drainFetchedMails`。
### 7.3 新增预报确认导入(核心交易)
```
运营:有效货件 + PENDING_CONFIRM
→ /mails/[id]/confirm → ForecastConfirmView
→ POST /api/mails/:id/import
version + idempotency_key + F_TransMode + F_OperationType
+ container_header 白名单 + selected_row_indexes
→ ConfirmImport.execute(import/confirm.ts)
assertTransition → IMPORTING
checkContainerConflict / acquireContainerActiveLock
buildSaveContainerEntity → saveContainer
live: ccAuth + POST /Container/SaveContainer
mock: mockRegisterContainer
成功 SUCCESS + external_id
超时 TIMEOUT_UNKNOWN → import_compensation
```
Worker **禁止**走这条 SaveContainer。门禁:`forecast-confirm-gate.ts` / `canEnterForecastConfirm`。
渠道未映射须 `ack_unmapped_channels`。强制跳冲突:Admin + `ENABLE_FORCE_IMPORT` + 审计。
### 7.4 DO / 转仓 / 工单
确认页按 `mail_type` 与 `instruction_id` 分发:
- DO → `confirm-do-upload.ts` → `cc/do-upload.ts`(`SaveFieldValue` + `/learun/adms/annexes/upload`)
- 转仓 → `confirm-batch-transfer.ts` / `batch-transfer.ts`(默认 capability **mock**)
- 工单 → `confirm-work-order.ts`
自动执行:`auto-exec.ts` + `ENABLE_AUTO_EXEC_*`;新增预报仍以人工确认为准。
### 7.5 补偿与超时回收
- `IMPORTING` 超过 `IMPORTING_TIMEOUT_MS`(默认 180s)→ reaper 打回
- `PARSING` 超过 `PARSING_STALE_MS`(默认 600s)→ 回 FETCHED
- 补偿 due:`processDueCompensations` 最多 `COMPENSATION_MAX_RETRY=3`
- IMAP 连续失败 ≥3 且配置了 `ALERT_WEBHOOK_URL` → POST JSON(15 分钟去重)
---
## 8. 配置与环境管理
### 8.1 切换方式
无独立 `config/dev.yml`。环境 = `.env` + 设置页 DB(IMAP/CC/OAuth **DB 优先于 env**)。
| 场景 | 做法 |
|------|------|
| 本地 | `.env` 指向 `localhost:7023`;`CC_MOCK=true` |
| Docker | compose 覆盖 `DATABASE_URL=@mysql:3306` |
| 生产 | `NODE_ENV=production` + 强密钥;弱口令 **拒绝启动**(`env.ts` `enforceProductionSecrets`) |
| TEST | `TEST_MODE=1`;与 `CC_MOCK` 正交 |
换 `SESSION_SECRET`:全员登出,且 **邮箱密码密文无法解密,须重新保存**。
### 8.2 环境变量(`getEnv` / `.env.example`)
| 键 | 含义 |
|----|------|
| `DATABASE_URL` | Prisma MySQL |
| `POLL_INTERVAL_MS` | Worker 间隔回退;设置页优先 |
| `DATA_RETENTION_DAYS` | 保留天数;Worker 不再自动删,用 `pnpm retention:cleanup` |
| `ENABLE_TYPE_OVERRIDE` | Admin 改邮件类型 |
| `ENABLE_FORCE_IMPORT` | 强制跳过柜冲突 |
| `ENABLE_ISO_CHECK` | 柜号 ISO6346 |
| `IMAP_*` | 主机/端口/用户/超时/lookback/每轮上限/IDLE/代理 |
| `CC_API_BASE` `CC_SAAS_HEADER` `CC_LOGIN_MARK` | SaaS |
| `CC_USERNAME` `CC_PASSWORD_PLAIN` `CC_PASSWORD_MD5` | 回退凭证 |
| `CC_MOCK` | 全局写 mock |
| `CC_HTTP_TIMEOUT_MS` `CC_READ_TIMEOUT_MS` | 写/读超时 |
| `APP_ADMIN_*` `APP_OPS_*` | **实际登录账号** |
| `SESSION_SECRET` | ≥32;会话 + AES 派生 |
| `TEST_MODE` | Mock IMAP/CC 故障注入 |
| `ALERT_WEBHOOK_URL` | 告警 |
| `OCR_PROVIDER` | `off\|local\|aliyun` |
| `OAUTH_*` | Gmail/MS 应用 |
| `ENABLE_AUTO_EXEC_*` `AUTO_EXEC_REQUIRE_CONFIRM` | 自动执行开关 |
| `SHIPMENT_ROW_SOFT/HARD_LIMIT` `ATTACHMENT_MAX_BYTES` | 清单/附件上限 |
### 8.3 端口约定(红线)
Web **3100**;MySQL 对外 **7023**。不要改成 3306/3000 当本地默认。
---
## 9. 构建、测试与部署
### 9.1 脚本(`package.json`)
| 脚本 | 作用 |
|------|------|
| `dev` / `dev:stack` | web+worker |
| `dev:web` / `dev:turbo` | 仅 Next |
| `build` | `next build` + `tsc -p tsconfig.worker.json` |
| `start` | `next start -p 3100` |
| `worker` | `tsx src/worker/index.ts` |
| `lint` | `next lint` |
| `test` / `test:coverage` / `test:all` | vitest |
| `test:e2e` / `test:e2e:test-mode` | Playwright |
| `db:*` | generate / migrate / seed / push |
| `cc:smoke` `imap:smoke` `imap:poll` | 联调 |
| `compose:*` | docker |
| `retention:cleanup` | 清旧数据 |
| `postinstall` | `prisma generate` |
### 9.2 日常命令
```bash
pnpm install
cp .env.example .env
docker compose up -d mysql
pnpm exec prisma db push
pnpm db:seed
pnpm dev # http://localhost:3100 admin/admin123
pnpm test
pnpm test:e2e # 需已起服务或交给 webServer 拉 pnpm dev
pnpm build && pnpm start
```
Windows:`.\docs\start-system.ps1`(`-Mode compose` 需 Docker Hub)。
### 9.3 部署要点
- 必须 **web + worker** 都活;worker 建议**单副本**(全局 `imap_poll` 锁)
- 持久化:MySQL 卷 + `./data`(eml、附件、`imap-runtime.json`)
- 生产:`pnpm db:migrate`(`migrate deploy`),不要用弱口令 seed 覆盖生产
- 健康检查:`GET /api/health`
- compose worker 命令必须是 `tsx src/worker/index.ts`(dist 里 `@/` 别名会 MODULE_NOT_FOUND)
测试目录:`src/**/*.test.ts`、`tests/unit`、`tests/integration/pipeline-fixtures.test.ts`、`tests/e2e`。覆盖率排除 `src/app` 与 `src/components`。
---
## 10. 代码规范与约定
来源:`docs/邮箱项目.md` §4–5、源码实践。
| 项 | 约定 |
|----|------|
| 语言 | TypeScript strict;API 入参 Zod |
| 命名 | 文件 kebab-case;函数 camelCase;Prisma model PascalCase;表 snake_case |
| 路径别名 | `@/` = `src/` |
| API | `ok`/`fail`;错误码大写下划线(`UNAUTHORIZED`、`VERSION_CONFLICT`) |
| 日志 | pino 对象字段 + 短消息(`worker.imap_tick`);禁止打完整密码/token |
| UI 文案 | `src/constants/ui-copy.ts`;中文 |
| CC 柜头覆盖 | 仅 `pickContainerHeaderPatch` 白名单 |
| 读盘 | 只走 `resolveDataFile` / `assertUnderRoot` |
| 锁 | 只走 `db-lock.ts` |
| 状态 | 只走 `assertTransition` |
| 包管理 | 仅 pnpm |
| 提交 | 用户明确要求才 git commit |
| Lint | `next lint`;无 Prettier/Ruff 工程配置 |
| 破坏性变更 | 先改 `docx` 再改代码 |
角色:ops 列表/详情/确认/重解析/下载;admin 另含改类型、设置、立即拉取、忽略、FORCE 导入。
---
## 11. 现有问题与注意事项
源码几乎无 `TODO/FIXME/HACK` 标记。已知项来自 README、协作文档与代码对照。
| ID | 说明 |
|----|------|
| AUTH-TABLE | `app_user` 被 seed,登录却只认 env。改密码易误改库表无效 |
| MOCK≠TEST | `CC_MOCK` 或设置页 Mock 的成功 **不算** 上线验收 |
| CAP-MOCK | `transfer`/`hold_split`/`label`/`customer_message` 默认 mock,V1.73 无正式表 |
| OCR-LOCAL | `OCR_PROVIDER=local` 是启发式,不是商用 OCR;aliyun 路径未当正式 SDK 宣传 |
| RATE-LIMIT | 限流是进程内 Map,多副本不共享 |
| LOCK | Prisma 池 GET_LOCK 会泄漏 → 永久 `lock_busy`;用 `scripts/clear-imap-lock.ts` |
| UNIT-DRIFT | README:`mail-intent` / `extract-header` / `four-business` / `work-order-dedupe` 与 expected 不完全一致 |
| COMP-SCOPE | 工单/DO/转仓 5xx 未统一进预报补偿表,靠详情一键重试 |
| DOCKER-PORT | Dockerfile EXPOSE 3000 vs 应用 3100 |
| HEALTH-PUBLIC | `/api/health` 无鉴权(docker 依赖) |
| FILTER | 关键字未命中 `IGNORED`/`SKIP_FILTER` 不进列表;`UNKNOWN` 仍可能 `PARSED` 供人工处理 |
| PARTIAL-CC | CC 对象有 data 但仍可能 `TIMEOUT_UNKNOWN` 进补偿(缺 external id) |
高耦合:`ParsePipeline` 体量大(分类+xlsx+zip+OCR+船司匹配+指令拆分);`ConfirmImport` 与 CC 字段、状态机、锁强绑定。
风险区:`SESSION_SECRET` 兼会话与邮箱密文;worker 多开抢锁;附件路径穿越;CC `api_base` SSRF(已有白名单)。
红线(事故级,见 `docs/邮箱项目.md` §5):Worker 禁止新增预报 SaveContainer;不绕过状态机;不用 Prisma 池 GET_LOCK;mock 不当验收;zip 只解一层;冲突默认阻断;先落库再 `\Seen`;端口 7023/3100;密钥不入库。
---
## 12. 外部集成
### 12.1 CarrierCentral SaaS(核心)
| 项 | 位置 |
|----|------|
| 基址 | `CC_API_BASE` 或 `cc_settings.api_base`(默认 `https://test.saas.carriercentral.vip/api`) |
| 头 | `Saas`、登录 token(`cc/auth.ts` `customerLogin`) |
| 契约 | `docx/接口/carriercentral客户端通用接口V1.73.md` |
| 登录 | `customerLogin`;token 缓存 `cc_token_cache` |
| 预报写入 | `POST /Container/SaveContainer`(`save-container.ts`) |
| 船司 | `GET GetShippingLineList`(`shipping-line.ts`) |
| DO | `SaveFieldValue` + `/learun/adms/annexes/upload` |
| Mock | `cc/mock-target-api.ts` `mock-state.ts`;设置页或 `CC_MOCK` |
| 主机约束 | `lib/cc-api-base.ts`:`*.carriercentral.vip` / localhost / 与 env 同主机 |
CC 密码协议为 MD5;设置页明文则服务端哈希。Token 可能出现在 GET query,日志须脱敏。
### 12.2 IMAP 邮箱
| 项 | 位置 |
|----|------|
| 协议 | IMAPS 993(预设 `lib/imap-presets.ts`:QQ/163/Gmail/Outlook) |
| 客户端 | `imapflow`(`imap/client.ts`) |
| 凭证 | 设置页 AES 密文,或 `.env IMAP_USER/PASS` |
| OAuth | Google / Microsoft(`oauth/providers.ts`);回调写 `mailbox_account` |
| 代理 | `IMAP_PROXY`;默认仅 Gmail/Outlook 主机走代理 |
### 12.3 OCR
`OCR_PROVIDER`:`off` | `local`(tesseract.js)| `aliyun`(endpoint/ak 环境变量)。解析辅助,不是主识别。
### 12.4 告警 Webhook
`ALERT_WEBHOOK_URL`:IMAP 连续失败或 CC 鉴权失败 POST JSON。空则只打日志。实现 `services/alert/webhook.ts`。
### 12.5 未集成
支付、短信、邮件 SMTP 发送、对象存储 SDK、LangChain、微信客服。附件只落本地 `data/`。
---
## 附录 A. 限制常量
| 常量 | 默认 | 位置 |
|------|------|------|
| 拉取间隔 | 30min(设置页) | `imap_settings` / `POLL_INTERVAL_MS` |
| lookback | 3 天 | `IMAP_LOOKBACK_DAYS` |
| 每箱每轮最多新信 | 100 | `IMAP_MAX_FETCH_PER_TICK` |
| 解析过期 | 600s | `PARSING_STALE_MS` |
| 导入超时 | 180s | `IMPORTING_TIMEOUT_MS` |
| 补偿次数 | 3 | `COMPENSATION_MAX_RETRY` |
| 货件行软/硬上限 | 1000 / 5000 | env |
| 附件 | 20MiB | `ATTACHMENT_MAX_BYTES` |
| 登录/导入限流 | 10/分钟 | login / import route |
| pull_log | ~1000 | `imap/pull-log.ts` |
| 间隔合法范围 | 3min~7d | 设置页 IMAP |
---
## 附录 B. 与助手仓对照(迁移功能时用)
| 维度 | yx(本文) | carriercentral-ai-assistant |
|------|------------|------------------------------|
| 形态 | Web 运营后台 | Windows Tkinter Demo |
| 主路径 | IMAP → 解析 → 确认 → SaveContainer | 自然语言 → ReAct → GET intelligent |
| CC 写 | 有(确认后) | 查询为主,附件下载 |
| 状态机 | 邮件 12 态 | 无邮件态 |
| 鉴权 | admin/ops Cookie | 无登录 |
| 禁止 | Worker 写新增预报 | 改 kf-ai、伪装客服 |
迁功能原则:复用 **CC 契约与字段名**(`F_ContainerNo`、`SaveContainer`),不要把 Next/Prisma/IMAP 整包塞进助手;UI 保持助手现有 `ui_main.py` 分层,除非用户确认换栈。
---
## 附录 C. 后续开发引用协议
之后任何需求(含「把 yx 功能加到助手仓」):
1. 先引用本文的**模块路径与函数名**,再给修改方案。
2. 遵循现有分层、Zod 入参、状态机、pnpm、中文 `ui-copy`。
3. 未经明确允许,不引入 Redis/新 ORM/第二套前端框架。
4. Worker 与 ConfirmImport 职责拆分不破。
5. 行为变更同步 `docx` 与本文对应章节;协作入口 `docs/邮箱项目.md` 红线保持。