# 项目全貌与开发指南 > **文档定位**:`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` 红线保持。