敖欣乐 1 miesiąc temu
rodzic
commit
f18696bf41
25 zmienionych plików z 2401 dodań i 862 usunięć
  1. 245 0
      docs/DEMO_NANCHI_REUSE_SPEC.md
  2. 149 0
      docs/email-import-for-lead-discovery.md
  3. 225 0
      docs/email-import-module-reuse.md
  4. 566 0
      docs/lead-detail-reuse-mapping.md
  5. 7 0
      lead-discovery/package-lock.json
  6. 1 0
      lead-discovery/package.json
  7. 17 2
      lead-discovery/src/app/app.component.ts
  8. 0 145
      lead-discovery/src/app/components/screen-result-card/screen-result-card.component.html
  9. 0 266
      lead-discovery/src/app/components/screen-result-card/screen-result-card.component.scss
  10. 0 181
      lead-discovery/src/app/components/screen-result-card/screen-result-card.component.ts
  11. 18 0
      lead-discovery/src/app/models/lead.model.ts
  12. 9 8
      lead-discovery/src/app/pages/dashboard/dashboard.component.html
  13. 2 1
      lead-discovery/src/app/pages/dashboard/dashboard.component.scss
  14. 211 20
      lead-discovery/src/app/pages/dashboard/dashboard.component.ts
  15. 179 170
      lead-discovery/src/app/pages/lead-detail/lead-detail.component.html
  16. 34 1
      lead-discovery/src/app/pages/lead-detail/lead-detail.component.scss
  17. 181 43
      lead-discovery/src/app/pages/lead-detail/lead-detail.component.ts
  18. 1 1
      lead-discovery/src/app/pages/list/list.component.ts
  19. 1 1
      lead-discovery/src/app/pages/product-catalog/product-catalog.component.ts
  20. 26 14
      lead-discovery/src/app/services/ai-screen.service.ts
  21. 112 0
      lead-discovery/src/app/services/credibility-analysis.service.ts
  22. 14 3
      lead-discovery/src/app/services/generate-api.service.ts
  23. 141 0
      lead-discovery/src/app/services/lead-detail-analysis.service.ts
  24. 29 6
      lead-discovery/src/app/services/mock-data.service.ts
  25. 233 0
      lead-discovery/src/app/services/parse-data.service.ts

+ 245 - 0
docs/DEMO_NANCHI_REUSE_SPEC.md

@@ -0,0 +1,245 @@
+# demo-nanchi 复用规格说明(AI 参考用)
+
+> 本文档为可直接发给 AI 的完整复用规格,用于在 demo-nanchi 中实现:**数据库连接**、**邮件解析、保存、展示** 及 **线索流程**。  
+> 参考 ltc-nanchi 的 `email-import` 模块,前端页面与交互保持不变。
+
+---
+
+## 〇、数据库连接(Parse / fmode-ng)
+
+### 初始化(参考 ltc-nanchi)
+
+在 `app.component.ts` 中完成 Parse 初始化:
+
+```ts
+import { FmodeParse } from 'fmode-ng/core';
+
+const Parse = FmodeParse.with('nova');           // app ID,与后端一致
+localStorage.setItem('company', 'VwQQMBm0mt');   // 租户/公司 ID
+```
+
+- **nova**:Parse 应用 ID,与 fmode 后端配置一致
+- **company**:租户 ID,按实际环境修改
+- 其他页面通过 `import { FmodeParse } from 'fmode-ng/core'; const Parse = FmodeParse.with('nova');` 使用
+
+### 使用方式(ltc-nanchi rules/parse.md)
+
+- 查询:`new Parse.Query('Email').equalTo('messageId', id).first()`
+- 保存:`const obj = new Parse.Object('Email'); obj.set('field', value); await obj.save();`
+- 用户:需登录后 `Parse.User.current()` 才能正确写入权限
+
+### Parse 表结构(Email / Lead)
+
+- **Email**:messageId, fromEmail, fromName, toEmails, ccEmails, subject, content, preview, receivedAt, customerEmail, attachments, source, hasScreened, leadId, screenResult
+- **Lead**:leadNumber, contactName, contactEmail, companyName, country, source, persona, valueGrade, quickScreenResult, followUpStage, entityId 等(与 `Lead` 接口字段一致)
+
+---
+
+## 一、目标流程
+
+```
+EML 文件 → Brainwork 解析 → 保存 Email → 填充 importData → 快速筛选 → 创建 Lead → 邮件可显示(Drawer / Inbox)
+```
+
+**关键点**:EML 解析后必须**保存邮件对象**,并在创建 Lead 后建立 Lead ↔ Email 关联,以便「查看原始邮件」、收件箱等能正确显示。
+
+---
+
+## 二、外部 API(仅配置)
+
+### 2.1 Brainwork EML 解析
+
+| 项目 | 值 |
+|------|-----|
+| URL | `https://eml.brainwork.club:8900/api/parse` |
+| 方法 | POST |
+| Body | FormData,字段名 `file`,值为 .eml 文件 |
+| 认证 | Header: `X-API-Key: fmode_cb_tgUyPSUXWZeLCgXVoLoCfOBingKQ9yAp` |
+| 参数 | Query: `analyze_attachments=true` |
+
+**响应**:
+```json
+{
+  "success": true,
+  "error": null,
+  "data": {
+    "messageId": "string",
+    "subject": "string",
+    "date": "string (ISO)",
+    "senderName": "string",
+    "senderEmail": "string",
+    "toList": [{ "name": "string", "email": "string" }],
+    "ccList": [{ "name": "string", "email": "string" }],
+    "cleanedBody": "string",
+    "customerEmail": "string",
+    "contactName": "string",
+    "companyName": "string",
+    "companyDomain": "string",
+    "title": "string",
+    "direction": "inbound|outbound|unknown",
+    "productDemands": [{
+      "productName": "string",
+      "quantity": "string",
+      "unitPrice": "string",
+      "totalPrice": "string",
+      "moq": "string",
+      "specifications": "string"
+    }],
+    "attachments": [{ "filename": "string", "mimeType": "string", "size": "number" }],
+    "attachmentStats": { "total": 0, "analyzed": 0, "skipped": 0, "bodyAnalyzed": false }
+  }
+}
+```
+
+### 2.2 Generate API(server.fmode.cn)
+
+| 项目 | 值 |
+|------|-----|
+| Base URL | `https://server.fmode.cn` |
+| 认证 | `Authorization: Bearer <token>`,token 来源 `localStorage.fmode_auth_token` |
+| 接口 | `/api/apig/generate/plugin/google-search`、`/api/apig/firecrawl/batch/scrape`、`/api/apig/generate/minor/{model}` |
+
+---
+
+## 三、数据契约(必须遵守)
+
+### 3.1 Email(保存与展示用)
+
+前端 Email 模型定义(`lead.model.ts`):
+
+```ts
+interface Email {
+  id: string;
+  from: string;           // 发件人邮箱,用于与 Lead.contactEmail 匹配
+  fromName: string;
+  to: string;
+  cc?: string;
+  subject: string;
+  preview: string;        // 正文摘要,约 200 字
+  body: string;
+  htmlContent?: string;
+  attachments: EmailAttachment[];
+  receivedAt: Date;
+  hasScreened: boolean;
+  screenResult?: QuickScreenResult;
+  leadId?: string;        // 创建 Lead 后写入
+}
+
+interface EmailAttachment {
+  id: string;
+  fileName: string;
+  fileSize: number;
+  mimeType: string;
+  icon: string;           // 如 'picture_as_pdf' | 'table_chart' | 'insert_drive_file'
+}
+```
+
+### 3.2 EmlParseData → Email 映射
+
+| EmlParseData | Email 字段 | 说明 |
+|--------------|------------|------|
+| `data.messageId` 或 `eml-${Date.now()}` | `id` | 唯一 ID |
+| `data.customerEmail` 或 `data.senderEmail` | `from` | **必须与 Lead.contactEmail 一致才能匹配** |
+| `data.contactName` 或 `data.senderName` | `fromName` | |
+| `data.toList.map(t => t.email).join(', ')` 或 `toList[0]?.email` | `to` | |
+| `data.ccList.map(t => t.email).join(', ')` | `cc` | 可选 |
+| `data.subject` | `subject` | |
+| `data.cleanedBody.slice(0, 200)` | `preview` | |
+| `data.cleanedBody` | `body` | |
+| `data.attachments` → 见下表 | `attachments` | |
+| `data.date ? new Date(data.date) : new Date()` | `receivedAt` | |
+| `false` | `hasScreened` | 筛选完成后可改为 true |
+| `undefined` | `leadId` | 创建 Lead 后赋值 `lead.id` |
+
+**attachments 映射**:
+- `id`: `att-${index}-${Date.now()}` 或类似
+- `fileName`: `att.filename`
+- `fileSize`: `att.size`
+- `mimeType`: `att.mimeType`
+- `icon`: 按 mimeType 映射,例如 `application/pdf` → `picture_as_pdf`,`application/vnd.openxmlformats-*` / `*spreadsheet*` → `table_chart`,否则 `insert_drive_file`
+
+### 3.3 importData(表单填充)
+
+```ts
+{
+  companyName: string;   // data.companyName || extractCompanyFromDomain(data.companyDomain)
+  contactName: string;   // data.contactName || data.senderName
+  roleDescription: string; // data.title
+  email: string;         // data.customerEmail || data.senderEmail
+  website: string;       // data.companyDomain ? `https://${data.companyDomain}` : ''
+}
+```
+
+**重要**:`importData.email` 必须等于 `Email.from`,这样创建 Lead 时 `lead.contactEmail = importData.email`,前端 `getEmailForLead(lead)` 用 `emails.find(e => e.from === lead.contactEmail)` 才能正确关联。
+
+### 3.4 Lead ↔ Email 关联
+
+- `getEmailForLead(lead)`:`emails.find(e => e.from === lead.contactEmail)`
+- `getLeadForEmail(email)`:`leads.find(l => l.contactEmail === email.from)`
+- 创建 Lead 后:`email.leadId = lead.id`,`lead.contactEmail = importData.email`(来自 `QuickScreenService.createLeadFromScreen` 的逻辑)
+
+---
+
+## 四、邮件展示位置(前端已有)
+
+| 位置 | 数据来源 | 使用字段 |
+|------|----------|----------|
+| **Email Drawer**(从看板「查看原始邮件」打开) | `drawerEmail`,来自 `getEmailForLead(lead)` | from, fromName, to, cc, subject, receivedAt, attachments, body |
+| **Inbox 列表** | `emails` 数组 | id, fromName, receivedAt, subject, preview, attachments.length, leadId |
+| **Inbox 详情** | `selectedInboxEmail` | subject, from, fromName, to, cc, receivedAt, attachments, body |
+
+---
+
+## 五、实现要点
+
+### 5.1 EML 导入后必须执行的步骤
+
+1. 调用 Brainwork API 解析 EML
+2. **构建 Email 对象**(按 3.2 映射)
+3. **将 Email 加入 `emails` 数组**(或通过 API 保存后加入)
+4. 填充 `importData`(companyName, contactName, roleDescription, email, website)
+5. 保存 `importedProducts = data.productDemands`、`importedEmailId = data.messageId`
+
+### 5.2 创建 Lead 后必须执行的步骤
+
+1. 调用 `QuickScreenService.createLeadFromScreen(screenResult)` 得到 `CreateLeadResult`
+2. 将 `lead` 加入 `leads` 数组
+3. **找到对应的 Email**(`emails.find(e => e.from === importData.email)`)并设置 `email.leadId = lead.id`
+4. 可选:`email.hasScreened = true`,`email.screenResult = screenResult`
+
+### 5.3 存储方式
+
+- **内存/Mock**:`emails.push(newEmail)`,`MockDataService.emails` 或组件内 `emails` 数组
+- **后端**:提供 `POST /api/emails` 保存 Email,返回完整 Email;创建 Lead 后 `PATCH /api/emails/:id` 更新 `leadId`。返回结构须与 `Email` 接口一致。
+
+---
+
+## 六、ltc-nanchi 参考(Email 持久化)
+
+ltc-nanchi 使用 Parse 存储:
+
+- **Email 表**:messageId, subject, content, html, fromEmail, fromName, toEmails, ccEmails, customerEmail, customerCompany, receivedAt, direction, status, attachments, aiAnalysis(Pointer→EmailAnalysis)等
+- **EmailAnalysis 表**:email(Pointer), summary, intent, requirementExtraction, verificationInfo 等
+
+demo-nanchi 的 `Email` 接口是简化版,后端若用 Parse 或自建表,需在返回时做字段映射,保证与上述 `Email` 契约一致。
+
+---
+
+## 七、配置 Checklist
+
+- [ ] Brainwork API 可访问,Key 有效
+- [ ] Generate API Token 已写入 `localStorage.fmode_auth_token`
+- [ ] 实现 EmlParseData → Email 转换并加入 emails
+- [ ] 创建 Lead 后更新对应 Email 的 `leadId`(及可选 `hasScreened`、`screenResult`)
+
+---
+
+## 八、相关文件路径
+
+| 用途 | 路径 |
+|------|------|
+| Email / Lead 模型 | `lead-discovery/src/app/models/lead.model.ts` |
+| Dashboard(EML 导入、Drawer、Inbox) | `lead-discovery/src/app/pages/dashboard/` |
+| 快速筛选 | `lead-discovery/src/app/services/quick-screen.service.ts` |
+| AI 筛查 | `lead-discovery/src/app/services/ai-screen.service.ts` |
+| 原始 ltc-nanchi 能力 | `docs/email-import-module-reuse.md` |

+ 149 - 0
docs/email-import-for-lead-discovery.md

@@ -0,0 +1,149 @@
+# demo-nanchi 邮件导入·后端对接复用文档
+
+> **前置约定**:前端页面、组件、交互流程**均保持不变**,内容已确定。  
+> 本文档面向**后端/服务对接**,说明为支撑现有前端所需的外部 API、配置及数据契约。
+
+---
+
+## 一、前端不变前提下的对接范围
+
+| 项目 | 说明 |
+|------|------|
+| 页面 | Dashboard、lead-detail、list 等保持不变 |
+| 组件 | 导入区域、筛选卡片、背调进度等保持不变 |
+| 数据流 | EML 导入 → 解析 → 填充表单 → 快速筛选 → 创建 Lead → 进入看板 |
+| 对接重点 | 外部 API 配置、数据格式约定、后端需提供的接口(若有持久化) |
+
+---
+
+## 二、外部 API 依赖(仅配置即可)
+
+### 2.1 Brainwork EML 解析(前端直接调用)
+
+| 配置项 | 值 | 说明 |
+|--------|-----|------|
+| URL | `https://eml.brainwork.club:8900/api/parse` | 固定 |
+| 方法 | POST (FormData: `file`) | - |
+| 认证 | Header: `X-API-Key: fmode_cb_tgUyPSUXWZeLCgXVoLoCfOBingKQ9yAp` | 需有效 |
+| 参数 | Query: `analyze_attachments=true` | 启用附件产品提取 |
+
+**返回结构(前端已按此解析)**:
+
+```ts
+{
+  success: boolean;
+  error: string | null;
+  data: {
+    messageId: string;
+    subject: string;
+    senderName: string;
+    senderEmail: string;
+    toList: { name: string; email: string }[];
+    ccList: { name: string; email: string }[];
+    cleanedBody: string;
+    customerEmail: string;
+    contactName: string;
+    companyName: string;
+    companyDomain: string;
+    title: string;
+    direction: string;
+    productDemands?: Array<{
+      productName: string;
+      quantity?: string;
+      unitPrice?: string;
+      totalPrice?: string;
+      moq?: string;
+      specifications?: string;
+    }>;
+    attachmentStats?: { total: number; analyzed: number; skipped: number; bodyAnalyzed: boolean };
+  };
+}
+```
+
+### 2.2 Generate API(server.fmode.cn)
+
+| 配置项 | 值 | 说明 |
+|--------|-----|------|
+| Base URL | `https://server.fmode.cn` | 固定 |
+| 认证 | `Authorization: Bearer <token>` | `localStorage.fmode_auth_token` |
+
+| 接口 | 路径 | 用途 |
+|------|------|------|
+| Google 搜索 | `/api/apig/generate/plugin/google-search` | 快速筛查、背调 |
+| Firecrawl 抓取 | `/api/apig/firecrawl/batch/scrape` | 官网内容抓取 |
+| AI 模型 | `/api/apig/generate/minor/{model}` | 分析推理 |
+
+### 2.3 可选:官网产品爬虫
+
+| 配置项 | 值 | 说明 |
+|--------|-----|------|
+| URL | `https://scraper.brainwork.club:8445/api/ecommerce` | POST `{ url: string }` |
+| 用途 | 若后续扩展「官网产品抓取」功能时使用 | 当前前端未接入 |
+
+---
+
+## 三、前端已用的数据契约(不可变更)
+
+### 3.1 importData(EML 解析后填充)
+
+```ts
+{
+  companyName: string;
+  contactName: string;
+  roleDescription: string;
+  email: string;
+  website: string;
+}
+```
+
+映射关系:`EmlParseData.companyName/companyDomain` → `companyName`,`customerEmail` → `email`,`companyDomain` → `website` 等(前端已实现)。
+
+### 3.2 QuickScreenResult(快速筛选输出)
+
+前端依赖 `QuickScreenService.screenManual()` → `QuickScreenResult` → `createLeadFromScreen()` → `Lead`。
+
+`QuickScreenResult` 须包含:`companyName`, `domain`, `country`, `persona`, `personaLabel`, `personaConfidence`, `valueGrade`, `productRelevance`, `mainBusiness`, `estimatedScale`, `certRequirements`, `recommendedAction`, `suggestedProducts`, `scores`, `demandSnapshot`, `credibilityDetails`, `recommendedActions`, `socialLinks`, `confidence`, `processingTime` 等(参见 `lead.model.ts`)。
+
+### 3.3 Lead(创建后的线索)
+
+前端展示依赖 `Lead` 的完整结构,包括 `quickScreenResult`, `backgroundCheckReport`, `aiResearchReport` 等。若后端提供 Lead 持久化,返回结构须与 `Lead` 接口一致。
+
+---
+
+## 四、后端可选能力(对接时需满足的契约)
+
+若需持久化,后端需提供以下能力,且**响应格式须与前端模型一致**,否则需在网关层做适配。
+
+### 4.1 表/实体参考(ltc-nanchi)
+
+| 表 | 关键字段 | 与 Lead 的对应 |
+|----|----------|----------------|
+| Email | messageId, subject, content, customerEmail, customerCompany, attachments | 导入来源,可关联 leadId |
+| EmailAnalysis | emailId, summary, intent, requirementExtraction, verificationInfo | 可映射到 quickScreenResult 等 |
+| Lead | 与 `Lead` 接口字段对齐 | 直接映射 |
+
+### 4.2 接口约定(示例)
+
+- `POST /api/leads`:创建 Lead,请求体与 `Lead` 兼容,返回完整 Lead。
+- `GET /api/leads/:id`:返回与 `Lead` 兼容的对象。
+- `GET /api/emails/:id`:若需要,返回与 `Email` 兼容的对象。
+
+具体路径和字段以实际后端设计为准,此处仅说明前端期望的结构。
+
+---
+
+## 五、配置 Checklist(前端不变场景)
+
+| 配置 | 说明 | 必选 |
+|------|------|------|
+| Brainwork API 可用 | `eml.brainwork.club:8900` 可访问,Key 有效 | ✅ |
+| Generate API Token | `localStorage.fmode_auth_token` 已设置 | ✅ |
+| 官网爬虫 | `scraper.brainwork.club:8445` | 可选 |
+
+---
+
+## 六、参考
+
+- 全量能力说明:`docs/email-import-module-reuse.md`
+- 前端模型:`lead-discovery/src/app/models/lead.model.ts`
+- 前端服务:`ai-screen.service.ts`、`generate-api.service.ts`、`quick-screen.service.ts`

+ 225 - 0
docs/email-import-module-reuse.md

@@ -0,0 +1,225 @@
+# Email Import 模块复用文档
+
+> 本文档总结 `nanchi/ltc-nanchi` 项目中 `email-import` 组件的**邮件解析**和 **AI 分析**所用到的服务、API、数据表和字段,供另一项目复用参考。
+
+---
+
+## 一、整体流程概览
+
+```
+EML 文件 → 解析 → 保存 Email + EmailAnalysis → 联网验证(可选)→ 产品提取 → Agent 价值分析 → 背调
+```
+
+---
+
+## 二、邮件解析相关
+
+### 2.1 核心服务
+
+| 服务 | 路径 | 职责 |
+|------|------|------|
+| `EmlImportService` | `shared/services/eml-import.service.ts` | EML 解析主入口,整合云端 API + 本地解析 |
+| `BrainworkEmlService` | `shared/services/brainwork-eml.service.ts` | 云端智能解析(元数据 + 正文 + 产品需求) |
+| `AttachmentUploaderService` | `shared/services/attachment-uploader/attachment-uploader.service.ts` | 附件上传到云存储 |
+
+### 2.2 邮件解析 API
+
+| API | 地址 | 方法 | 用途 |
+|-----|------|------|------|
+| Brainwork 解析 | `https://eml.brainwork.club:8900/api/parse` | POST (FormData) | 解析 EML,提取元数据、正文、产品需求等 |
+| 认证方式 | Header: `X-API-Key` | - | `fmode_cb_tgUyPSUXWZeLCgXVoLoCfOBingKQ9yAp` |
+| 深度分析参数 | `analyze_attachments=true` | Query | 启用附件产品需求提取 |
+
+**请求示例:**
+```
+POST /api/parse?analyze_attachments=true
+Headers: X-API-Key: <API_KEY>
+Body: FormData { file: <EML File> }
+```
+
+**返回结构 (`BrainworkParseData`):**
+- `messageId`, `subject`, `date`, `senderEmail`, `senderName`
+- `toList`, `ccList` - `{ name, email }[]`
+- `cleanedBody` - 清洗后的正文
+- `customerEmail`, `contactName`, `companyName`, `companyDomain`, `title`
+- `direction`: `'inbound' | 'outbound' | 'unknown'`
+- `productDemands` - 产品需求数组(含 `productName`, `quantity`, `unitPrice`, `totalPrice`, `moq`, `specifications` 等)
+
+### 2.3 本地解析
+
+- 使用 `postal-mime` 库解析 EML 获取附件二进制,用于上传
+- 使用 `EmlImportService` 中的 `buildParticipantsFromApi` 等辅助方法构建参与人、线程信息
+
+### 2.4 附件上传
+
+- 存储:`NovaStorage`(fmode-ng)
+- 服务:`AttachmentUploaderService.uploadMultiple()`
+- 附件分析:可选 AI 分析,EML 导入时通常 `enableAIAnalysis: false`,直接使用 Brainwork 结果
+
+---
+
+## 三、AI 分析相关
+
+### 3.1 核心服务
+
+| 服务 | 路径 | 职责 |
+|------|------|------|
+| `EmailImportWorkflowService` | `shared/services/agent/email-import-workflow.service.ts` | 统一工作流:解析 → 保存 → 联网验证 |
+| `GenerateApiService` | `shared/services/agent/generate-api.service.ts` | 调用 server.fmode.cn 的 Google Search、AI、Firecrawl |
+| `AiTaskFlowService` | `shared/services/agent/ai-task-flow.service.ts` | AI 任务流编排 |
+| `AgentValueAnalysisService` | `shared/services/agent/agent-value-analysis.service.ts` | 线索价值分析(五维度评估) |
+
+### 3.2 Generate API(server.fmode.cn)
+
+**Base URL**: `https://server.fmode.cn`  
+**认证**: `Authorization: Bearer <sessionToken>` 或 `localStorage.fmode_auth_token`
+
+| 接口 | 路径 | 方法 | 用途 |
+|------|------|------|------|
+| Google 搜索 | `/api/apig/generate/plugin/google-search` | POST | 公司验证、市场情报 |
+| AI 模型 | `/api/apig/generate/minor/{model}` | POST | 分析、推理(如 `gemini-2.5-flash`) |
+| Firecrawl 批量抓取 | `/api/apig/firecrawl/batch/scrape` | POST | 抓取官网等网页 Markdown/JSON |
+
+**Google Search 请求:**
+```json
+{ "query": "string", "num": 5, "page": 1 }
+```
+
+**AI Model 请求:**
+```json
+{ "content": "用户消息", "role_content": "系统角色" }
+```
+
+**Firecrawl 批量抓取请求:**
+```json
+{
+  "urls": ["https://example.com"],
+  "formats": ["markdown", { "type": "json", "prompt": "...", "schema": {...} }],
+  "pollInterval": 2,
+  "timeout": 120
+}
+```
+
+### 3.3 产品提取相关 API
+
+| API | 地址 | 方法 | 用途 |
+|-----|------|------|------|
+| 官网爬虫 | `https://scraper.brainwork.club:8445/api/ecommerce` | POST | 从官网 URL 提取产品列表 |
+
+**请求示例:**
+```json
+{ "url": "https://example.com" }
+```
+
+**返回字段(产品相关)**:`products[]` 含 `name`, `price`, `category`, `description`, `specifications`, `certifications`;另有 `success`, `method`, `storeUrl`, `domainCorrected`, `socialLinks` 等
+
+### 3.4 产品提取逻辑(email-import 组件内)
+
+- **邮件产品**:优先用 `aiClassification.requirementExtraction._rawDemands`(Brainwork)+ 附件 `productMentions` + 关键词匹配(First Aid Kit, IFAK, Trauma Kit 等)
+- **官网产品**:调用爬虫 API,映射为 `ProductItem`
+
+---
+
+## 四、数据表与字段
+
+### 4.1 Email
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `messageId` | string | 消息 ID |
+| `inReplyTo`, `references`, `threadKey` | string/array | 线程 |
+| `fromEmail`, `fromName`, `toEmails`, `ccEmails`, `bccEmails` | string/array | 收发件人 |
+| `participants` | array | 参与人 `{ email, name, type, side }` |
+| `subject`, `content`, `html` | string | 主题、正文 |
+| `receivedAt` | Date | 接收时间 |
+| `direction` | string | inbound/outbound/unknown |
+| `status` | string | unread 等 |
+| `customerEmail`, `customerName`, `ourEmail`, `ourName` | string | 客户/我方识别 |
+| `customerCompany` | string | 客户公司 |
+| `priority` | string | high/medium/low |
+| `source` | string | imported_eml / imported_eml_email_import |
+| `suggestedRoleDescription` | string | 角色推断 |
+| `customerType`, `country`, `region`, `isGovernment` | string/boolean | AI 画像 |
+| `companyScale`, `decisionMakerLevel`, `buyingPower` | string | 公司规模等 |
+| `attachments` | array | 附件列表 |
+| `aiAnalysis` | Pointer→EmailAnalysis | 关联分析对象 |
+
+### 4.2 EmailAnalysis
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `email` | Pointer→Email | 关联邮件 |
+| `summary` | string | 摘要 |
+| `intent` | string | 意图分类 |
+| `requirementExtraction` | object | 需求提取 `{ productCategory, budget, quantity, urgency, _rawDemands }` |
+| `customerProfile` | object | 客户画像 |
+| `sentimentScore`, `confidence`, `leadPotential` | number | 情绪、置信度、线索潜力 |
+| `nextSteps` | array | 建议行动 |
+| `comprehensiveAnalysis` | object | 综合分析(含 verification) |
+| `verificationInfo` | object | 联网验证结果 |
+| `attachmentAnalysis` | object | 附件分析 |
+| `attachments` | array | 附件列表 |
+
+### 4.3 Enterprise
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `email` | string | 唯一标识(客户邮箱) |
+| `name` | string | 公司名称 |
+| `industry`, `status` | string | 行业、状态 |
+| `basic_info` | object | domain, country, address, foundedYear, employeeCount, revenue 等 |
+| `enterpriseProfile` | object | 企业画像、可信度 |
+| `verificationData` | object | 联网验证数据 sources, companyProfile, keyFindings 等 |
+| `marketIntelligence` | object | 市场情报(可选) |
+
+### 4.4 ContactInfo
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `contactEmail`, `contactName` | string | 联系方式 |
+| `phone`, `title` | string | 电话、职位 |
+| `enterprise` | Pointer→Enterprise | 关联企业 |
+
+---
+
+## 五、联网验证流程(EmailImportWorkflowService)
+
+1. 从 `customerEmail` / `fromEmail` 提取域名
+2. 构建官网 URL:`https://{domain}`, `https://www.{domain}`
+3. 调用 `GenerateApiService.firecrawlBatchScrape()` 抓取 + 结构化提取(JSON schema 含 company_name, industry, country, founded_year, employee_count 等)
+4. 调用 `GenerateApiService.callAIModel('gemini-2.5-flash', ...)` 分析抓取内容,得到 `WebSearchVerificationResult`
+5. 更新 `EmailAnalysis.verificationInfo` 和 `comprehensiveAnalysis.verification`
+6. 调用 `EnterpriseContactService.createOrUpdateEnterprise()` 更新/创建 Enterprise
+
+---
+
+## 六、Agent 价值分析(AgentValueAnalysisService)
+
+1. **快速背调**:Google Search + Firecrawl 抓取公司官网
+2. **结构化提取**:从抓取内容中提取 country, industry, employee_count, founded_year, certifications 等
+3. **AI 五维度评估**:`completionJSON`(fmode-1.6-cn)结合线索信息 + 背调数据输出:
+   - `recommendation`: `'continue' | 'abandon'`
+   - `confidence`, `valueLevel`
+   - `dimensions`: match, opportunity, urgency, credibility, risk
+   - `reportMarkdown`, `nextActions`
+   - `quickBgCheck`: 快速背调摘要
+
+---
+
+## 七、依赖与包
+
+- `fmode-ng`:Parse、FmodeParse、completionJSON、NovaStorage、FmodeChatCompletion
+- `postal-mime`:本地 EML 解析
+- Angular:HttpClient、Material 组件等
+
+---
+
+## 八、复用 Checklist
+
+- [ ] 配置 Brainwork API Key(`eml.brainwork.club`)
+- [ ] 配置 Generate API BaseUrl + 认证(server.fmode.cn)
+- [ ] 配置官网爬虫 API(scraper.brainwork.club)
+- [ ] 创建 Parse/后端表:Email, EmailAnalysis, Enterprise, ContactInfo
+- [ ] 实现或复用 EmlImportService、EmailImportWorkflowService、AgentValueAnalysisService
+- [ ] 实现附件上传存储(NovaStorage 或等价方案)
+- [ ] 可选:调整产品匹配关键词、公司域名白名单(OUR_COMPANY_DOMAINS)

+ 566 - 0
docs/lead-detail-reuse-mapping.md

@@ -0,0 +1,566 @@
+# lead-detail 板块与 ltc-nanchi 复用映射
+
+> 本文档根据 `nanchi/ltc-nanchi` 的数据模型和接口,为 demo-nanchi 的 `lead-detail` 各板块提供精准数据获取的复用方案。  
+> 目标:每个板块的数据应来自 ltc-nanchi 的真实模型与 API,而非 mock 或本地推断。
+
+---
+
+## 一、lead-detail 板块概览
+
+| 板块 | 位置 | 当前 demo 数据来源 | 应复用的 ltc-nanchi 能力 |
+|------|------|-------------------|--------------------------|
+| 1. 客户画像分析 | 左列 | `lead.quickScreenResult`(快速筛选) | EmailAnalysis.customerProfile + requirementExtraction |
+| 2. 快速筛选结果(公司/联系人) | 左列 | `lead.quickScreenResult` | EmailAnalysis + Enterprise + ContactInfo |
+| 3. 需求摘要 | 左列 | `getDemandSnapshot()`(基于 suggestedProducts 推断) | EmailAnalysis.requirementExtraction + Brainwork productDemands |
+| 4. 原始邮件信息 | 左列 | `getEmailForLead(lead)` → Email | Parse `Email` 表 |
+| 5. 推荐产品 | 右列 | `lead.quickScreenResult.suggestedProducts` | EmailAnalysis + 产品匹配服务 |
+| 6. 可信度 / 评分 / AI 下一步 | 右列 | 本地 `getCredibilityLevel()` / `getScores()` 推断 | Enterprise.enterpriseProfile + AgentValueAnalysisService |
+| 7. 销售验证 | 右列 | `lead.personaVerified` / `lead.salesFeedback` | Lead 表 + 人工反馈 |
+| 8. 深度调研 / 背调报告 | 右列 | `lead.aiResearchReport` / `lead.backgroundCheckReport` | LeadBackgroundCheckService + reportData |
+
+---
+
+## 二、Parse 表与模型复用
+
+### 2.1 Email 表
+
+| Parse 字段 | 对应 demo Email 字段 | lead-detail 使用场景 |
+|------------|----------------------|----------------------|
+| `messageId` | `id` | 唯一标识 |
+| `fromEmail` | `from` | 发件人,用于 `getEmailForLead(lead)` 匹配 |
+| `fromName` | `fromName` | 原始邮件板块 |
+| `toEmails` | `to` | 原始邮件板块 |
+| `ccEmails` | `cc` | 原始邮件板块 |
+| `subject` | `subject` | 原始邮件板块 |
+| `content` / `html` | `body` | 原始邮件板块 |
+| `receivedAt` | `receivedAt` | 原始邮件板块 |
+| `attachments` | `attachments` | 原始邮件板块 |
+| `aiAnalysis` | Pointer→EmailAnalysis | 关联分析结果 |
+| `leadId` | `leadId` | 创建 Lead 后写入,用于关联 |
+| `customerEmail`, `customerCompany` | 用于关联 Enterprise/Contact | 查询时辅助匹配 |
+
+**复用接口:**
+
+- 查询:`new Parse.Query('Email').equalTo('messageId', id).first()` 或 `.equalTo('leadId', leadId)`
+- 服务参考:`EmailImportWorkflowService` 保存 Email 后建立 `aiAnalysis` 关联
+
+---
+
+### 2.2 EmailAnalysis 表
+
+| Parse 字段 | 对应 demo 板块 | 说明 |
+|------------|----------------|------|
+| `email` | - | Pointer→Email |
+| `summary` | 客户画像 / 快速筛选 | 摘要 |
+| `intent` | 客户画像 | 意图分类 |
+| `requirementExtraction` | 需求摘要、推荐产品 | 见下表 |
+| `customerProfile` | 客户画像、可信度 | companyType, country, buyingPower 等 |
+| `verificationInfo` | 可信度、评分 | 联网验证结果 |
+| `comprehensiveAnalysis` | 综合评估 | 含 verification、nextSteps |
+| `confidence`, `leadPotential` | 评分详情 | 0-100 |
+| `nextSteps` | AI 推荐下一步 | 建议行动数组 |
+| `attachmentAnalysis` | 需求摘要 | 附件中的产品需求 |
+
+**requirementExtraction 结构(EmailAnalysisWorkflowService):**
+
+```ts
+interface RequirementExtractionResult {
+  productCategory: string[];   // 产品类别 → 需求摘要 products、推荐产品
+  quantity: string;            // 数量 → 需求摘要 quantity
+  budget: string;              // 预算
+  urgency: 'urgent' | 'normal' | 'flexible';
+  targetMarket: string;
+  certifications: string[];    // → quickScreenResult.certRequirements
+  keyRequirements: string[];
+  mustHaveComponents: string[];
+  colorPreference: string[];
+  sizeRequirement: string;
+}
+```
+
+**Brainwork 扩展 `_rawDemands`:**
+
+- 来源:`EmlImportService` 将 Brainwork `productDemands` 写入 `requirementExtraction._rawDemands`
+- 结构:`{ productName, quantity, unitPrice, totalPrice, moq, specifications }[]`
+- 用途:需求摘要中的「产品 × 数量」、预估年采购额
+
+---
+
+### 2.3 Enterprise 表
+
+| Parse 字段 | 对应 demo 板块 | 说明 |
+|------------|----------------|------|
+| `email` | - | 唯一标识(客户邮箱域名对应公司) |
+| `name` | 公司信息 | 公司名称 |
+| `industry` | 公司信息 | 行业 |
+| `basic_info` | 公司信息 | domain, country, address, foundedYear, employeeCount, revenue |
+| `enterpriseProfile` | 可信度、评分 | credibilityScore, companyScale, certifications |
+| `verificationData` | 可信度信号 | companyVerified, sources, keyFindings, lastVerified |
+| `marketIntelligence` | 可选 | 市场情报 |
+
+**enterpriseProfile 示例:**
+
+```ts
+{
+  companyScale: { employeeCount, employeeRange },
+  establishment: { foundedYear },
+  financialStrength: { purchasePower },
+  certifications: { certList, hasCertifications },
+  credibilityScore: { overall, dimensions }
+}
+```
+
+---
+
+### 2.4 ContactInfo 表
+
+| Parse 字段 | 对应 demo 板块 | 说明 |
+|------------|----------------|------|
+| `name`, `realname` | 联系人信息 | 姓名 |
+| `data.email` | 联系人信息 | 邮箱 |
+| `data.title` | 联系人信息 | 职位 |
+| `data.companyName` | 公司信息 | 关联公司 |
+| `mobile` | 联系人信息 | 电话 |
+| `enterprise` | - | Pointer→Enterprise |
+| `contactProfile` | 可信度 | position.level, decisionMaker, linkedinVerified |
+| `verificationData` | 可信度信号 | 联网验证结果 |
+
+---
+
+### 2.5 Lead 表(ltc-nanchi)
+
+| Parse 字段 | 对应 demo Lead | 说明 |
+|------------|----------------|------|
+| `leadNumber` | `leadNumber` | 线索编号 |
+| `contactName`, `contactEmail` | 同 | 联系人 |
+| `companyName`, `country` | 同 | 公司 |
+| `persona`, `valueGrade` | 同 | 画像、价值等级 |
+| `quickScreenResult` | 同 | 快速筛选结果(可存 JSON) |
+| `followUpStage` | 同 | 跟进阶段 |
+| `entityId`, `domainKey`, `emailKey` | 同 | 主体关联 |
+| `enterprise` | - | Pointer→Enterprise |
+| `contact` | - | Pointer→ContactInfo |
+| `investigationSummary` | 背调 / 可信度 | enterprise/contact 的可信度汇总 |
+
+---
+
+## 三、AI 模型与接口
+
+### 3.1 模型概览
+
+| 模型 | 来源 | 用途 | lead-detail 对应板块 |
+|------|------|------|----------------------|
+| **fmode-1.6-cn** | fmode-ng 内部 | 轻量级任务、线索价值分析、综合评级 | 客户画像、可信度、评分、AI 下一步、背调综合评级 |
+| **gemini-2.5-flash** | server.fmode.cn | 搜索增强、深度分析、联网验证、战略分析 | 可信度/联网验证、推荐产品、背调维度分析 |
+| **fmode-4.5-1m-tiny** | fmode-ng 内部 | 公司背调结构化提取、长上下文 | 背调报告 |
+| **gemini-1.5-flash-latest** | server.fmode.cn | 长上下文分析(备用) | 背调等 |
+
+**模型选型策略(AiTaskFlowService):**
+
+- 轻量级(预处理、提取)→ `fmode-1.6-cn`(中文优化、快速、低成本)
+- 搜索增强(验证、情报)→ `google-search` + `firecrawl` + `gemini-2.5-flash`
+- 深度分析(战略、方案)→ `gemini-2.5-flash`
+
+---
+
+### 3.2 Generate API(server.fmode.cn)
+
+**Base URL**:`https://server.fmode.cn`  
+**认证**:`Authorization: Bearer <token>`,token 来自 `localStorage.fmode_auth_token` 或 `Parse.User.current().get('sessionToken')`
+
+| 接口 | 路径 | 方法 | 模型/插件 | 用途 |
+|------|------|------|-----------|------|
+| **Google 搜索** | `/api/apig/generate/plugin/google-search` | POST | - | 公司验证、市场情报、竞品分析 |
+| **AI 模型** | `/api/apig/generate/minor/{model}` | POST | gemini-2.5-flash 等 | 深度分析、联网验证、战略评估 |
+| **Firecrawl 批量抓取** | `/api/apig/firecrawl/batch/scrape` | POST | - | 抓取官网/社媒等网页 Markdown 或 JSON |
+
+**Google Search 请求:**
+
+```json
+{ "query": "string", "num": 5, "page": 1 }
+```
+
+**AI Model 请求:**
+
+```json
+{ "content": "用户消息", "role_content": "系统角色" }
+```
+
+**Firecrawl 批量抓取请求:**
+
+```json
+{
+  "urls": ["https://example.com"],
+  "formats": ["markdown", { "type": "json", "prompt": "...", "schema": {...} }],
+  "pollInterval": 2,
+  "timeout": 120
+}
+```
+
+---
+
+### 3.3 内部 AI 接口(fmode-ng)
+
+| 接口 | 来源 | 默认模型 | 用途 |
+|------|------|----------|------|
+| **completionJSON** | `fmode-ng/core` | fmode-1.6-cn | 结构化 JSON 输出,用于价值分析、提取等 |
+| **FmodeChatCompletion** | `fmode-ng/core` | fmode-1.6-cn | 对话式补全,支持流式 |
+| **executeSingleAITask** | AiTaskFlowService | 按任务类型选型 | 统一任务编排,自动选模型 |
+
+**completionJSON 用法:**
+
+```ts
+import { completionJSON } from 'fmode-ng/core';
+
+const result = await completionJSON(prompt, schema, onStream?);
+// 返回 schema 定义的结构化对象
+```
+
+**executeSingleAITask 用法:**
+
+```ts
+const result = await aiTaskFlowService.executeSingleAITask<OutputType>(
+  taskType,  // 如 'requirement_extraction' | 'company_verification' | 'strategic_analysis'
+  { message, schema, searchQueries?, ... }
+);
+```
+
+---
+
+### 3.4 Brainwork EML 解析 API
+
+| 项目 | 值 |
+|------|-----|
+| URL | `https://eml.brainwork.club:8900/api/parse` |
+| 方法 | POST(FormData,字段 `file` = .eml 文件) |
+| 认证 | Header: `X-API-Key: fmode_cb_tgUyPSUXWZeLCgXVoLoCfOBingKQ9yAp` |
+| 参数 | Query: `analyze_attachments=true` |
+
+**返回**:含 `productDemands`、`cleanedBody`、`senderEmail`、`companyDomain` 等,映射到 requirementExtraction._rawDemands。
+
+---
+
+### 3.5 官网爬虫 API
+
+| 项目 | 值 |
+|------|-----|
+| URL | `https://scraper.brainwork.club:8445/api/ecommerce` |
+| 方法 | POST |
+| Body | `{ "url": "https://example.com" }` |
+
+**返回**:`products[]`(name, price, category, certifications)、`socialLinks` 等,用于推荐产品、需求补充。
+
+---
+
+### 3.6 板块与 AI 模型 / 接口映射
+
+| lead-detail 板块 | 使用的 AI 模型 | 调用的 API / 服务 |
+|------------------|----------------|-------------------|
+| 客户画像分析 | fmode-1.6-cn、gemini-2.5-flash | EmailAnalysisWorkflowService、AiTaskFlowService |
+| 快速筛选结果 | fmode-1.6-cn、gemini-2.5-flash | EmailAnalysisWorkflowService、loadEnterpriseAndContactInfo |
+| 需求摘要 | Brainwork、fmode-1.6-cn | Brainwork parse、requirementExtraction、_rawDemands |
+| 原始邮件信息 | 无 | Parse Email 表查询 |
+| 推荐产品 | fmode-1.6-cn、gemini-2.5-flash | generateProductSuggestions、官网爬虫 API |
+| 可信度 / 评分 | gemini-2.5-flash | GenerateApiService.callAIModel、firecrawlBatchScrape、googleSearch |
+| AI 推荐下一步 | fmode-1.6-cn | AgentValueAnalysisService(completionJSON)、EmailAnalysis.nextSteps |
+
+**demo-nanchi 实现**:
+- `AiScreenService`:有联网背调数据时用 gemini-2.5-flash,否则用 fmode-1.6-cn(completionJSON)
+- `CredibilityAnalysisService`:独立可信度/评分分析,使用 gemini-2.5-flash
+- 所有提示词已统一为**南驰(Nanchi)**
+| 背调报告 | fmode-1.6-cn、gemini-2.5-flash、fmode-4.5-1m-tiny | LeadBackgroundCheckService、AiTaskFlowService |
+
+---
+
+## 四、服务与接口复用
+
+### 4.1 EnterpriseContactService
+
+**路径**:`ltc-nanchi/shared/services/enterprise-contact.service.ts`
+
+| 方法 | 用途 | lead-detail 调用场景 |
+|------|------|----------------------|
+| `loadEnterpriseAndContactInfo(message)` | 按邮件加载 Enterprise + ContactInfo + verificationInfo | 进入详情页时,用 `email` 或 `lead.contactEmail` 查 Email,再传入 message 获取 enterpriseInfo、contactInfo、verificationInfo |
+| `createOrUpdateEnterprise(params, aiAnalysisData)` | 创建/更新企业 | EML 导入、联网验证后 |
+| `createOrUpdateContact(params, aiAnalysisData)` | 创建/更新联系人 | EML 导入、联网验证后 |
+| `getEnterpriseAndContact(email, companyName)` | 直接查 Enterprise/Contact | 已有 lead 时,用 contactEmail 查 |
+| `buildInvestigationSummary(enterprise, contact)` | 构建背调维度摘要 | 可信度板块 |
+
+**loadEnterpriseAndContactInfo 返回:**
+
+```ts
+{
+  enterpriseInfo?: {
+    name, industry, status,
+    basic_info: { domain, country, address, foundedYear, employeeCount, revenue },
+    verificationData,
+    enterpriseProfile,
+    marketIntelligence,
+    needsVerification, verificationAge
+  };
+  contactInfo?: {
+    name, realname, mobile,
+    data: { email, title, companyName, ... },
+    verificationData,
+    contactProfile
+  };
+  verificationInfo?: {
+    companyVerified,
+    dataSources: { url, title, snippet, credibility }[],
+    keyFindings, companyProfile, recentActivity,
+    verifiedInfo: { country, employeeCount, foundedYear, certifications }
+  };
+}
+```
+
+---
+
+### 4.2 EmailAnalysisWorkflowService
+
+**路径**:`ltc-nanchi/shared/services/agent/email-analysis-workflow.service.ts`
+
+| 产出 | 对应 lead-detail 板块 |
+|------|------------------------|
+| `requirementExtraction` | 需求摘要、推荐产品、certRequirements |
+| `customerProfile` | 客户画像、mainBusiness、country |
+| `strategicAnalysis`(leadScore, priorityLevel, responseTime) | 评分、AI 下一步 |
+| `suggestedProducts` | 推荐产品列表 |
+| `summary`, `intent` | 客户画像摘要 |
+
+**调用方式**:通常由 Email 导入/分析流程触发,结果写入 EmailAnalysis 表。demo 侧通过查询 `Email.aiAnalysis` 或 `EmailAnalysis` 获取。
+
+---
+
+### 4.3 AgentValueAnalysisService
+
+**路径**:`ltc-nanchi/shared/services/agent/agent-value-analysis.service.ts`
+
+| 产出 | 对应 lead-detail 板块 |
+|------|------------------------|
+| `dimensions` | 评分详情(match, opportunity, urgency, credibility, risk) |
+| `quickBgCheck` | 可信度信号(companyVerified, riskLevel, certifications) |
+| `nextActions` | AI 推荐下一步 |
+| `reportMarkdown` | 背调报告摘要 |
+| `recommendation` | 是否继续跟进 |
+
+**映射到 demo 五维评分:**
+
+| demo 评分 | AgentValueAnalysis 维度 |
+|-----------|-------------------------|
+| 需求明确度 | dimensions.match |
+| 可信度 | dimensions.credibility |
+| 商业规模 | dimensions.opportunity |
+| 供需匹配 | dimensions.match / productFit |
+| (未单独) | dimensions.urgency, dimensions.risk |
+
+---
+
+### 4.4 EmailImportWorkflowService
+
+**路径**:`ltc-nanchi/shared/services/agent/email-import-workflow.service.ts`
+
+- 负责:解析 → 保存 Email + EmailAnalysis → 联网验证 → 创建/更新 Enterprise + ContactInfo
+- demo 复用:EML 导入时调用等价流程,确保 Email、EmailAnalysis、Enterprise、ContactInfo 四表数据完整
+
+---
+
+### 4.5 LeadBackgroundCheckService(背调)
+
+**路径**:`ltc-nanchi/shared/services/background-check/lead-background-check.service.ts`
+
+- 产出:`reportData`(dimension1/2/3, rating, risk, strategy)
+- 对应 demo:`lead.backgroundCheckReport`(BackgroundCheckReport 结构)
+- 保存:reportData 写入 Lead,Enterprise 表更新 social_media 等
+
+---
+
+## 五、板块级数据获取流程
+
+### 5.1 客户画像分析
+
+**数据来源:**
+
+- `EmailAnalysis.customerProfile`:companyType, country, buyingPower, companyScale
+- `EmailAnalysis.requirementExtraction`:productCategory, certifications
+- `lead.quickScreenResult`:persona, personaLabel, matchedKeywords(可由 requirementExtraction + customerProfile 映射)
+
+**复用:**
+
+1. 查询 `Email` → `Email.aiAnalysis`(Pointer EmailAnalysis)
+2. 读取 `customerProfile`、`requirementExtraction`
+3. 若 Lead 已有 `quickScreenResult`,可优先用;否则从 EmailAnalysis 映射生成
+
+---
+
+### 5.2 快速筛选结果(公司信息 / 联系人信息)
+
+**公司信息:**
+
+- `Enterprise`:name, basic_info.domain, basic_info.country, industry, enterpriseProfile
+- `EmailAnalysis`:customerProfile.country, requirementExtraction
+- `Email`:customerCompany, companyDomain
+
+**联系人信息:**
+
+- `ContactInfo`:name, data.email, data.title, mobile
+- `Email`:fromEmail, fromName, customerEmail, contactName
+
+**复用:**
+
+1. `loadEnterpriseAndContactInfo(emailMessage)` 获取 enterpriseInfo、contactInfo
+2. 合并到 `quickScreenResult` 或单独展示
+
+---
+
+### 5.3 需求摘要
+
+**数据来源:**
+
+- `EmailAnalysis.requirementExtraction`:productCategory, quantity, budget, certifications
+- `requirementExtraction._rawDemands`(Brainwork):productName, quantity, unitPrice, totalPrice
+- `lead.quickScreenResult.demandSnapshot`:若快速筛选已生成则直接用
+
+**映射:**
+
+```ts
+// 从 requirementExtraction 构建 demandSnapshot
+demandSnapshot = {
+  products: (requirementExtraction._rawDemands || []).map(d => ({
+    name: d.productName,
+    quantity: parseQuantity(d.quantity),
+    unit: d.unit || '/年'
+  })),
+  estimatedAnnualValue: 从 totalPrice / budget 推算,
+  certifications: requirementExtraction.certifications || []
+};
+```
+
+---
+
+### 5.4 原始邮件信息
+
+**数据来源:** Parse `Email` 表
+
+**复用:**
+
+1. `getEmailForLead(lead)`:`emails.find(e => e.leadId === lead.id)` 或 `e.from === lead.contactEmail`
+2. 若内存无,则 `Parse.Query('Email').equalTo('leadId', lead.id).first()` 或 `.equalTo('customerEmail', lead.contactEmail)`
+3. 返回字段按 DEMO_NANCHI_REUSE_SPEC 的 Email 契约
+
+---
+
+### 5.5 推荐产品
+
+**数据来源:**
+
+- `EmailAnalysisWorkflowService.generateProductSuggestions(requirementExtraction)`:基于 productCategory
+- 产品目录服务:按 productCategory 匹配 SKU、name、price
+- `lead.quickScreenResult.suggestedProducts`:若快速筛选已生成
+
+**复用:**
+
+1. 优先用 `lead.quickScreenResult.suggestedProducts`
+2. 若无,从 `requirementExtraction.productCategory` + 产品目录匹配
+
+---
+
+### 5.6 可信度 / 评分 / AI 推荐下一步
+
+**可信度:**
+
+- `Enterprise.enterpriseProfile.credibilityScore`
+- `Enterprise.verificationData`:companyVerified, sources
+- `verificationInfo`:dataSources, verifiedInfo
+
+**评分:**
+
+- `AgentValueAnalysisService.analyze()` → dimensions
+- 或 `EmailAnalysis` 的 confidence、leadPotential、strategicAnalysis.leadScore
+
+**AI 下一步:**
+
+- `AgentValueAnalysisResult.nextActions`
+- `EmailAnalysis.nextSteps`
+- `lead.quickScreenResult.recommendedActions`(timeframe, filesToSend, keyPoints)
+
+**复用:**
+
+1. 有 Lead 且已做价值分析:用 AgentValueAnalysis 结果
+2. 否则用 `loadEnterpriseAndContactInfo` 的 verificationInfo + enterpriseProfile 推断
+
+---
+
+### 5.7 深度调研 / 背调报告
+
+**数据来源:**
+
+- `LeadBackgroundCheckService`:reportData → dimension1/2/3, rating, risk, strategy
+- `Enterprise`:enterpriseProfile, verificationData
+- `ContactInfo`:contactProfile
+
+**复用:**
+
+1. 背调完成后,reportData 写入 `lead.backgroundCheckReport`
+2. 结构已与 demo `BackgroundCheckReport` 兼容,可直接展示
+
+---
+
+## 六、实现优先级建议
+
+| 优先级 | 板块 | 复用内容 | 复杂度 |
+|--------|------|----------|--------|
+| P0 | 原始邮件信息 | Parse Email 表查询 | 低 |
+| P0 | 需求摘要 | EmailAnalysis.requirementExtraction + _rawDemands | 中 |
+| P1 | 快速筛选结果(公司/联系人) | loadEnterpriseAndContactInfo | 中 |
+| P1 | 客户画像 | EmailAnalysis.customerProfile + requirementExtraction | 中 |
+| P2 | 可信度 / 评分 | Enterprise.enterpriseProfile + verificationInfo | 中 |
+| P2 | AI 推荐下一步 | AgentValueAnalysis 或 EmailAnalysis.nextSteps | 中 |
+| P3 | 背调报告 | LeadBackgroundCheckService + reportData | 高 |
+
+---
+
+## 七、相关文件路径
+
+| 用途 | ltc-nanchi 路径 | demo-nanchi 路径 |
+|------|-----------------|------------------|
+| **AI / Generate API** | `shared/services/agent/generate-api.service.ts` | 可复制或封装调用 |
+| AI 任务流 | `shared/services/agent/ai-task-flow.service.ts` | executeSingleAITask 等 |
+| 企业/联系人服务 | `shared/services/enterprise-contact.service.ts` | 可复制或封装调用 |
+| 邮件分析工作流 | `shared/services/agent/email-analysis-workflow.service.ts` | 参考产出结构 |
+| 价值分析 | `shared/services/agent/agent-value-analysis.service.ts` | 可复制或封装调用 |
+| 邮件导入工作流 | `shared/services/agent/email-import-workflow.service.ts` | 参考完整流程 |
+| 分析类型定义 | `shared/services/agent/email-analysis.types.ts` | 可复制 RequirementExtractionResult 等 |
+| 背调服务 | `shared/services/background-check/lead-background-check.service.ts` | 参考 reportData 结构 |
+| fmode-ng 能力 | `fmode-ng/core`(completionJSON, FmodeChatCompletion) | 复用同包 |
+| lead-detail 页面 | `lead-discovery/src/app/pages/lead-detail/` | 待接入上述数据源 |
+
+---
+
+## 八、数据流小结
+
+```
+Email (Parse)
+    ├── aiAnalysis → EmailAnalysis
+    │       ├── requirementExtraction → 需求摘要、推荐产品、certRequirements
+    │       ├── customerProfile → 客户画像、公司信息
+    │       ├── verificationInfo → 可信度信号
+    │       └── nextSteps → AI 推荐下一步
+    ├── customerEmail / fromEmail → 关联
+    └── leadId → Lead
+
+Enterprise (Parse) ← loadEnterpriseAndContactInfo
+    ├── enterpriseProfile → 可信度、评分
+    ├── verificationData → 联网验证
+    └── basic_info → 公司信息
+
+ContactInfo (Parse) ← loadEnterpriseAndContactInfo
+    ├── data → 联系人信息
+    └── contactProfile → 可信度
+
+Lead (Parse)
+    ├── quickScreenResult → 快速筛选(可含 demandSnapshot, scores, recommendedActions)
+    ├── backgroundCheckReport → 背调报告
+    └── enterprise, contact → 关联 Enterprise/ContactInfo
+
+AgentValueAnalysisService.analyze() → dimensions, nextActions, quickBgCheck
+```

+ 7 - 0
lead-discovery/package-lock.json

@@ -19,6 +19,7 @@
         "@angular/platform-browser-dynamic": "^19.2.18",
         "@angular/router": "^19.2.18",
         "fmode-ng": "^0.0.245",
+        "postal-mime": "^2.7.3",
         "rxjs": "~7.8.0",
         "tslib": "^2.3.0",
         "zone.js": "~0.15.1"
@@ -14233,6 +14234,12 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/postal-mime": {
+      "version": "2.7.3",
+      "resolved": "https://registry.npmmirror.com/postal-mime/-/postal-mime-2.7.3.tgz",
+      "integrity": "sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw==",
+      "license": "MIT-0"
+    },
     "node_modules/postcss": {
       "version": "8.5.6",
       "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",

+ 1 - 0
lead-discovery/package.json

@@ -21,6 +21,7 @@
     "@angular/platform-browser-dynamic": "^19.2.18",
     "@angular/router": "^19.2.18",
     "fmode-ng": "^0.0.245",
+    "postal-mime": "^2.7.3",
     "rxjs": "~7.8.0",
     "tslib": "^2.3.0",
     "zone.js": "~0.15.1"

+ 17 - 2
lead-discovery/src/app/app.component.ts

@@ -1,9 +1,14 @@
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
 import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';
 import { MatToolbarModule } from '@angular/material/toolbar';
 import { MatIconModule } from '@angular/material/icon';
 import { MatButtonModule } from '@angular/material/button';
 import { MatBadgeModule } from '@angular/material/badge';
+import { FmodeParse } from 'fmode-ng/core';
+
+/** Parse 数据库初始化(参考 ltc-nanchi),需在 app 启动时执行一次 */
+const Parse = FmodeParse.with('nova');
+localStorage.setItem('company', 'VwQQMBm0mt');
 
 @Component({
     selector: 'app-root',
@@ -122,5 +127,15 @@ import { MatBadgeModule } from '@angular/material/badge';
     }
   `]
 })
-export class AppComponent {
+export class AppComponent implements OnInit {
+  constructor() {
+    this.initParse();
+  }
+
+  ngOnInit() {}
+
+  /** 初始化 Parse 连接(fmode 后端),其他页面直接 import Parse 使用 */
+  private initParse() {
+    console.log('[demo-nanchi] Parse initialized (fmode/nova)');
+  }
 }

+ 0 - 145
lead-discovery/src/app/components/screen-result-card/screen-result-card.component.html

@@ -1,145 +0,0 @@
-<div class="quick-assessment-card" [class]="'grade-border-' + result.valueGrade.toLowerCase()">
-  <!-- Header: Company + Country -->
-  <div class="card-header">
-    <div class="company-header">
-      <div class="company-avatar" [style.background]="getPersonaColor(result.persona)">
-        <mat-icon>{{ getPersonaIcon(result.persona) }}</mat-icon>
-      </div>
-      <div class="company-info">
-        <h3 class="company-name">{{ result.companyName }}</h3>
-        <div class="country-flag">{{ getFlag(result.country) }} {{ result.country }}</div>
-      </div>
-    </div>
-  </div>
-
-  <!-- Tags: Persona + Grade + Action -->
-  <div class="tags-row">
-    <span class="persona-badge" [style.background]="getPersonaBg(result.persona)" [style.color]="getPersonaColor(result.persona)">
-      <mat-icon class="badge-icon">{{ getPersonaIcon(result.persona) }}</mat-icon>
-      {{ result.persona }}-{{ result.personaLabel }}
-    </span>
-    <span class="grade-badge" [class]="'grade-' + result.valueGrade.toLowerCase()">
-      {{ result.valueGrade }}级
-    </span>
-    <span class="action-badge" [class]="'action-' + result.valueGrade.toLowerCase()">
-      {{ getActionLabel(result.valueGrade) }}
-    </span>
-  </div>
-
-  <!-- Demand Summary -->
-  <div class="demand-section" *ngIf="getDemandSnapshot()">
-    <div class="section-title">📦 需求摘要</div>
-    <div class="demand-list">
-      <div class="demand-item" *ngFor="let p of getDemandSnapshot()?.products">
-        <span class="demand-product">· {{ p.name }} × {{ p.quantity }}{{ p.unit }}</span>
-      </div>
-    </div>
-    <div class="estimated-value" *ngIf="getDemandSnapshot()?.estimatedAnnualValue">
-      💰 预估年采购额:${{ formatNumber(getDemandSnapshot()!.estimatedAnnualValue) }}+
-    </div>
-  </div>
-
-  <!-- Credibility -->
-  <div class="credibility-section">
-    <div class="section-title">✅ 可信度:{{ getCredibilityLevel() }}</div>
-    <div class="credibility-signals">
-      <span class="signal-item" *ngFor="let signal of getCredibilitySignals()">
-        · {{ signal }}
-      </span>
-    </div>
-  </div>
-
-  <!-- Matched Products -->
-  <div class="products-section" *ngIf="result.suggestedProducts.length > 0">
-    <div class="section-title">🎯 我方匹配产品</div>
-    <div class="matched-products">
-      <div class="matched-product-item" *ngFor="let p of result.suggestedProducts">
-        <span class="product-name">{{ p.name }}</span>
-        <span class="product-sku">({{ p.sku }})</span>
-        <span class="match-arrow">→</span>
-        <span class="match-reason">{{ p.reason || '匹配需求' }}</span>
-      </div>
-    </div>
-  </div>
-
-  <!-- Score Details (Expandable) -->
-  <mat-expansion-panel class="scores-panel" *ngIf="getScores()">
-    <mat-expansion-panel-header>
-      <mat-panel-title>
-        <mat-icon>assessment</mat-icon>
-        <span>📊 评分详情</span>
-      </mat-panel-title>
-    </mat-expansion-panel-header>
-    <div class="scores-grid">
-      <div class="score-item">
-        <span class="score-label">需求明确度</span>
-        <div class="score-bar-wrap">
-          <div class="score-bar" [style.width]="(getScores()?.demandClarity || 0) + '%'"></div>
-        </div>
-        <span class="score-value">{{ getScores()?.demandClarity || 0 }}分</span>
-      </div>
-      <div class="score-item">
-        <span class="score-label">可信度</span>
-        <div class="score-bar-wrap">
-          <div class="score-bar" [style.width]="(getScores()?.credibility || 0) + '%'"></div>
-        </div>
-        <span class="score-value">{{ getScores()?.credibility || 0 }}分</span>
-      </div>
-      <div class="score-item">
-        <span class="score-label">商业规模</span>
-        <div class="score-bar-wrap">
-          <div class="score-bar" [style.width]="(getScores()?.businessScale || 0) + '%'"></div>
-        </div>
-        <span class="score-value">{{ getScores()?.businessScale || 0 }}分</span>
-      </div>
-      <div class="score-item">
-        <span class="score-label">供需匹配</span>
-        <div class="score-bar-wrap">
-          <div class="score-bar" [style.width]="(getScores()?.productFit || 0) + '%'"></div>
-        </div>
-        <span class="score-value">{{ getScores()?.productFit || 0 }}分</span>
-      </div>
-      <div class="score-item">
-        <span class="score-label">紧迫度</span>
-        <div class="score-bar-wrap">
-          <div class="score-bar" [style.width]="(getScores()?.urgency || 0) + '%'"></div>
-        </div>
-        <span class="score-value">{{ getScores()?.urgency || 0 }}分</span>
-      </div>
-    </div>
-    <div class="total-score">
-      <span class="total-label">总分</span>
-      <span class="total-value">{{ getScores()?.total || 0 }}分</span>
-    </div>
-  </mat-expansion-panel>
-
-  <!-- Recommended Actions -->
-  <div class="actions-section" *ngIf="getRecommendedActions()">
-    <div class="section-title">📌 推荐行动</div>
-    <div class="action-content">
-      <span class="action-timeframe">{{ getRecommendedActions()?.timeframe }}</span>内发送:
-      <span class="action-files" *ngFor="let file of getRecommendedActions()?.filesToSend; let last = last">
-        {{ file }}<span *ngIf="!last"> + </span>
-      </span>
-    </div>
-  </div>
-
-  <!-- Action Buttons -->
-  <div class="button-row">
-    <button mat-stroked-button (click)="viewOriginal.emit()">
-      <mat-icon>email</mat-icon> 查看原文
-    </button>
-    <button mat-raised-button color="primary" (click)="deepAnalysis.emit()">
-      <mat-icon>search</mat-icon> 深度调研
-    </button>
-    <button mat-raised-button color="accent" (click)="createLead.emit()">
-      <mat-icon>check_circle</mat-icon> 确认并建档
-    </button>
-  </div>
-
-  <!-- Processing Time -->
-  <div class="processing-time" *ngIf="result.processingTime">
-    ⚡ 处理耗时:{{ formatProcessingTime(result.processingTime) }}秒
-  </div>
-</div>
-

+ 0 - 266
lead-discovery/src/app/components/screen-result-card/screen-result-card.component.scss

@@ -1,266 +0,0 @@
-.quick-assessment-card {
-  background: #fff;
-  border-radius: 16px;
-  padding: 24px;
-  box-shadow: 0 4px 16px rgba(0,0,0,0.1);
-  border-left: 4px solid #ccc;
-  max-width: 600px;
-}
-.grade-border-s { border-left-color: #c62828; }
-.grade-border-a { border-left-color: #e65100; }
-.grade-border-b { border-left-color: #f9a825; }
-.grade-border-c { border-left-color: #9e9e9e; }
-
-.card-header {
-  margin-bottom: 16px;
-}
-.company-header {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-.company-avatar {
-  width: 48px;
-  height: 48px;
-  border-radius: 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  flex-shrink: 0;
-}
-.company-avatar mat-icon {
-  font-size: 24px;
-  width: 24px;
-  height: 24px;
-  color: #fff;
-}
-.company-info {
-  flex: 1;
-}
-.company-name {
-  font-size: 20px;
-  font-weight: 600;
-  margin: 0 0 4px;
-  color: #1a1a2e;
-}
-.country-flag {
-  font-size: 14px;
-  color: #666;
-}
-
-.tags-row {
-  display: flex;
-  gap: 8px;
-  flex-wrap: wrap;
-  margin-bottom: 20px;
-}
-.persona-badge, .grade-badge, .action-badge {
-  display: inline-flex;
-  align-items: center;
-  gap: 4px;
-  padding: 4px 12px;
-  border-radius: 20px;
-  font-size: 12px;
-  font-weight: 600;
-}
-.badge-icon {
-  font-size: 14px;
-  width: 14px;
-  height: 14px;
-}
-.grade-s { background: #ffebee; color: #c62828; }
-.grade-a { background: #fff3e0; color: #e65100; }
-.grade-b { background: #fffde7; color: #f9a825; }
-.grade-c { background: #fafafa; color: #757575; }
-.action-s { background: #ffebee; color: #c62828; }
-.action-a { background: #fff3e0; color: #e65100; }
-.action-b { background: #fffde7; color: #f9a825; }
-.action-c { background: #fafafa; color: #757575; }
-
-.section-title {
-  font-size: 13px;
-  font-weight: 600;
-  color: #555;
-  margin-bottom: 8px;
-}
-
-.demand-section {
-  margin-bottom: 16px;
-  padding: 12px;
-  background: #f8f9fb;
-  border-radius: 10px;
-}
-.demand-list {
-  display: flex;
-  flex-direction: column;
-  gap: 4px;
-  margin-bottom: 8px;
-}
-.demand-item {
-  font-size: 14px;
-  color: #333;
-}
-.estimated-value {
-  font-size: 15px;
-  font-weight: 600;
-  color: #2e7d32;
-  margin-top: 8px;
-}
-
-.credibility-section {
-  margin-bottom: 16px;
-  padding: 12px;
-  background: #f0f4ff;
-  border-radius: 10px;
-}
-.credibility-signals {
-  display: flex;
-  flex-direction: column;
-  gap: 4px;
-  font-size: 13px;
-  color: #555;
-}
-.signal-item {
-  display: block;
-}
-
-.products-section {
-  margin-bottom: 16px;
-}
-.matched-products {
-  display: flex;
-  flex-direction: column;
-  gap: 8px;
-}
-.matched-product-item {
-  display: flex;
-  align-items: center;
-  gap: 6px;
-  font-size: 13px;
-  padding: 6px 10px;
-  background: #fafafa;
-  border-radius: 8px;
-}
-.product-name {
-  font-weight: 500;
-  color: #333;
-}
-.product-sku {
-  font-size: 11px;
-  color: #999;
-  font-family: monospace;
-}
-.match-arrow {
-  color: #999;
-}
-.match-reason {
-  color: #666;
-  font-size: 12px;
-}
-
-.scores-panel {
-  margin-bottom: 16px;
-  box-shadow: none;
-  border: 1px solid #e0e0e0;
-}
-.scores-grid {
-  display: flex;
-  flex-direction: column;
-  gap: 10px;
-  padding: 12px 0;
-}
-.score-item {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-.score-label {
-  font-size: 13px;
-  color: #666;
-  min-width: 80px;
-}
-.score-bar-wrap {
-  flex: 1;
-  height: 8px;
-  background: #f0f0f0;
-  border-radius: 4px;
-  overflow: hidden;
-}
-.score-bar {
-  height: 100%;
-  background: linear-gradient(90deg, #1976d2, #42a5f5);
-  border-radius: 4px;
-  transition: width 0.3s;
-}
-.score-value {
-  font-size: 13px;
-  font-weight: 600;
-  color: #1976d2;
-  min-width: 50px;
-  text-align: right;
-}
-.total-score {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 12px;
-  background: #f0f4ff;
-  border-radius: 8px;
-  margin-top: 8px;
-}
-.total-label {
-  font-size: 14px;
-  font-weight: 600;
-  color: #333;
-}
-.total-value {
-  font-size: 18px;
-  font-weight: 700;
-  color: #1976d2;
-}
-
-.actions-section {
-  margin-bottom: 16px;
-  padding: 12px;
-  background: #fff3e0;
-  border-radius: 10px;
-}
-.action-content {
-  font-size: 14px;
-  color: #333;
-  line-height: 1.6;
-}
-.action-timeframe {
-  font-weight: 600;
-  color: #e65100;
-}
-.action-files {
-  font-weight: 500;
-  color: #1976d2;
-}
-
-.button-row {
-  display: flex;
-  gap: 10px;
-  margin-top: 20px;
-  flex-wrap: wrap;
-}
-.button-row button {
-  font-size: 13px;
-}
-.button-row mat-icon {
-  font-size: 18px;
-  width: 18px;
-  height: 18px;
-  margin-right: 4px;
-}
-
-.processing-time {
-  text-align: center;
-  font-size: 12px;
-  color: #999;
-  margin-top: 12px;
-  padding-top: 12px;
-  border-top: 1px solid #eee;
-}
-

+ 0 - 181
lead-discovery/src/app/components/screen-result-card/screen-result-card.component.ts

@@ -1,181 +0,0 @@
-import { Component, Input, Output, EventEmitter } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { MatCardModule } from '@angular/material/card';
-import { MatIconModule } from '@angular/material/icon';
-import { MatButtonModule } from '@angular/material/button';
-import { MatChipsModule } from '@angular/material/chips';
-import { MatDividerModule } from '@angular/material/divider';
-import { MatProgressBarModule } from '@angular/material/progress-bar';
-import { MatTooltipModule } from '@angular/material/tooltip';
-import { MatExpansionModule } from '@angular/material/expansion';
-import {
-  QuickScreenResult, CATEGORY_LABELS, CATEGORY_ICONS,
-  CustomerPersona, PERSONA_DEFINITIONS, GRADE_LABELS
-} from '../../models/lead.model';
-
-// 扩展 QuickScreenResult 以包含五维评分(临时,后续应更新模型)
-interface ExtendedQuickScreenResult extends QuickScreenResult {
-  scores?: {
-    demandClarity: number;
-    credibility: number;
-    businessScale: number;
-    productFit: number;
-    urgency: number;
-    total: number;
-  };
-  demandSnapshot?: {
-    products: Array<{ name: string; quantity: number; unit: string }>;
-    estimatedAnnualValue: number;
-    certifications: string[];
-  };
-  credibilityDetails?: {
-    level: 'high' | 'medium' | 'low';
-    signals: string[];
-  };
-  recommendedActions?: {
-    timeframe: string;
-    filesToSend: string[];
-    keyPoints: string[];
-  };
-}
-
-@Component({
-    selector: 'app-screen-result-card',
-    imports: [
-        CommonModule, MatCardModule, MatIconModule, MatButtonModule,
-        MatChipsModule, MatDividerModule, MatProgressBarModule, MatTooltipModule,
-        MatExpansionModule,
-    ],
-    templateUrl: './screen-result-card.component.html',
-    styleUrl: './screen-result-card.component.scss'
-})
-export class ScreenResultCardComponent {
-  @Input() result!: QuickScreenResult;
-  @Output() createLead = new EventEmitter<void>();
-  @Output() deepAnalysis = new EventEmitter<void>();
-  @Output() viewOriginal = new EventEmitter<void>();
-  @Output() dismiss = new EventEmitter<void>();
-
-  getCatIcon(cat: string): string {
-    return CATEGORY_ICONS[cat as keyof typeof CATEGORY_ICONS] || 'business';
-  }
-
-  getPersonaColor(p: CustomerPersona): string {
-    return PERSONA_DEFINITIONS[p]?.color || '#999';
-  }
-
-  getPersonaBg(p: CustomerPersona): string {
-    return PERSONA_DEFINITIONS[p]?.bgColor || '#fafafa';
-  }
-
-  getPersonaIcon(p: CustomerPersona): string {
-    return PERSONA_DEFINITIONS[p]?.icon || 'business';
-  }
-
-  getFlag(country: string): string {
-    const flags: Record<string, string> = {
-      US: '🇺🇸', GB: '🇬🇧', DE: '🇩🇪', PL: '🇵🇱', CA: '🇨🇦',
-      AU: '🇦🇺', SE: '🇸🇪', CN: '🇨🇳',
-    };
-    return flags[country] || '🌍';
-  }
-
-  getActionLabel(grade: string): string {
-    return GRADE_LABELS[grade as keyof typeof GRADE_LABELS] || '';
-  }
-
-  // 临时方法:从 result 中提取或生成五维评分数据
-  getScores(): ExtendedQuickScreenResult['scores'] | undefined {
-    const extended = this.result as ExtendedQuickScreenResult;
-    if (extended.scores) {
-      return extended.scores;
-    }
-    // 如果没有,生成模拟数据(实际应从后端获取)
-    return {
-      demandClarity: 85,
-      credibility: 85,
-      businessScale: 75,
-      productFit: 80,
-      urgency: 50,
-      total: 76
-    };
-  }
-
-  getDemandSnapshot(): ExtendedQuickScreenResult['demandSnapshot'] | undefined {
-    const extended = this.result as ExtendedQuickScreenResult;
-    if (extended.demandSnapshot) {
-      return extended.demandSnapshot;
-    }
-    // 如果没有,从 suggestedProducts 生成
-    if (this.result.suggestedProducts.length > 0) {
-      return {
-        products: this.result.suggestedProducts.map(p => ({
-          name: p.name,
-          quantity: 1000,
-          unit: '/年'
-        })),
-        estimatedAnnualValue: this.result.suggestedProducts.reduce((sum, p) => sum + p.wholesalePrice * 1000, 0),
-        certifications: this.result.certRequirements
-      };
-    }
-    return undefined;
-  }
-
-  getCredibilityLevel(): 'high' | 'medium' | 'low' {
-    const extended = this.result as ExtendedQuickScreenResult;
-    if (extended.credibilityDetails) {
-      return extended.credibilityDetails.level;
-    }
-    // 根据域名判断
-    if (this.result.domain && !this.result.domain.includes('gmail') && !this.result.domain.includes('hotmail')) {
-      return 'high';
-    }
-    return 'medium';
-  }
-
-  getCredibilitySignals(): string[] {
-    const extended = this.result as ExtendedQuickScreenResult;
-    if (extended.credibilityDetails) {
-      return extended.credibilityDetails.signals;
-    }
-    // 生成默认信号
-    const signals: string[] = [];
-    if (this.result.domain && !this.result.domain.includes('gmail')) {
-      signals.push('企业邮箱');
-    }
-    if (this.result.confidence > 70) {
-      signals.push('完整签名块');
-    }
-    if (this.result.certRequirements.length > 0) {
-      signals.push('有认证要求');
-    }
-    return signals.length > 0 ? signals : ['基础信息完整'];
-  }
-
-  getRecommendedActions(): ExtendedQuickScreenResult['recommendedActions'] | undefined {
-    const extended = this.result as ExtendedQuickScreenResult;
-    if (extended.recommendedActions) {
-      return extended.recommendedActions;
-    }
-    // 根据等级生成默认行动
-    const timeframes: Record<string, string> = {
-      S: '24h内',
-      A: '3天内',
-      B: '7天内',
-      C: '归档观察'
-    };
-    return {
-      timeframe: timeframes[this.result.valueGrade] || '7天内',
-      filesToSend: ['产品目录', '报价单', '认证文件包'],
-      keyPoints: ['供应稳定性', '认证齐全']
-    };
-  }
-
-  formatNumber(value: number): string {
-    return new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(value);
-  }
-
-  formatProcessingTime(ms: number): string {
-    return (ms / 1000).toFixed(1);
-  }
-}

+ 18 - 0
lead-discovery/src/app/models/lead.model.ts

@@ -273,6 +273,17 @@ export interface BatchProcessResult {
   entities: EntityGroup[];
 }
 
+/** 提取的产品需求(来自 Brainwork EML 解析 / 正文·附件),用于需求摘要 */
+export interface ExtractedProductDemand {
+  productName: string;
+  quantity: string;
+  specifications?: string;
+  source?: string;
+  totalPrice?: string;
+  unitPrice?: string;
+  moq?: string;
+}
+
 // ===== Lead 对象 =====
 export interface Lead {
   id: string;
@@ -296,6 +307,9 @@ export interface Lead {
   personaVerifiedBy?: string;
   personaVerifiedAt?: Date;
 
+  /** 提取的产品需求(邮件/附件解析),需求摘要仅展示此项,不展示模拟数据 */
+  extractedProductDemands?: ExtractedProductDemand[];
+
   quickScreenResult: QuickScreenResult;
   quickScreenAt: Date;
 
@@ -460,6 +474,8 @@ export interface EmailAttachment {
   fileSize: number;
   mimeType: string;
   icon: string;
+  /** 预览/下载 URL,有则点击可跳转预览 */
+  url?: string;
 }
 
 // ===== 邮件对象(模拟) =====
@@ -478,6 +494,8 @@ export interface Email {
   hasScreened: boolean;
   screenResult?: QuickScreenResult;
   leadId?: string;
+  /** Parse 存储后的 objectId,用于 updateEmailLeadId */
+  parseObjectId?: string;
 }
 
 // ===== 产品目录 =====

+ 9 - 8
lead-discovery/src/app/pages/dashboard/dashboard.component.html

@@ -334,11 +334,12 @@
 
           <!-- Screen Result -->
           <div *ngIf="importScreenResult" class="import-result">
-            <app-screen-result-card
-              [result]="importScreenResult"
+            <app-lead-detail
+              [lead]="importScreenResult"
+              [mode]="'preview'"
               (createLead)="onCreateImportLead()"
-              (dismiss)="importScreenResult = null">
-            </app-screen-result-card>
+              (dismiss)="clearImport()">
+            </app-lead-detail>
           </div>
         </div>
 
@@ -907,7 +908,7 @@
           <span class="att-total-size">共 {{ getTotalSize(drawerEmail.attachments) }}</span>
         </div>
         <div class="drawer-att-grid">
-          <div class="drawer-att-card" *ngFor="let att of drawerEmail.attachments" (click)="downloadAttachment(att)">
+          <div class="drawer-att-card" *ngFor="let att of drawerEmail.attachments" (click)="downloadAttachment(att)" [title]="att.url ? '点击下载/预览附件' : '该附件暂无预览链接'">
             <div class="att-icon-wrap" [class]="getAttColorClass(att.mimeType)">
               <mat-icon>{{ att.icon }}</mat-icon>
             </div>
@@ -923,7 +924,7 @@
       <mat-divider></mat-divider>
 
       <div class="drawer-email-body">
-        <pre class="email-body-text">{{ drawerEmail.body }}</pre>
+        <div class="email-body-text content-html" [innerHTML]="formatEmailContent(drawerEmail)"></div>
       </div>
     </div>
   </div>
@@ -1012,7 +1013,7 @@
             <span class="att-total-size">共 {{ getTotalSize(selectedInboxEmail.attachments) }}</span>
           </div>
           <div class="inbox-att-list">
-            <div class="inbox-att-chip" *ngFor="let att of selectedInboxEmail.attachments" (click)="downloadAttachment(att)">
+            <div class="inbox-att-chip" *ngFor="let att of selectedInboxEmail.attachments" (click)="downloadAttachment(att)" [title]="att.url ? '点击下载/预览附件' : '该附件暂无预览链接'">
               <div class="att-icon-wrap" [class]="getAttColorClass(att.mimeType)">
                 <mat-icon>{{ att.icon }}</mat-icon>
               </div>
@@ -1029,7 +1030,7 @@
 
         <!-- Email Body -->
         <div class="inbox-detail-body">
-          <pre class="email-body-text">{{ selectedInboxEmail.body }}</pre>
+          <div class="email-body-text content-html" [innerHTML]="formatEmailContent(selectedInboxEmail)"></div>
         </div>
 
         <!-- Actions -->

+ 2 - 1
lead-discovery/src/app/pages/dashboard/dashboard.component.scss

@@ -564,13 +564,14 @@
   font-size: 14px;
   line-height: 1.7;
   color: #333;
-  white-space: pre-wrap;
   word-wrap: break-word;
   margin: 0;
   padding: 0;
   background: transparent;
   border: none;
 }
+.email-body-text.content-html { white-space: normal; }
+.email-body-text.content-html img { max-width: 100%; height: auto; }
 
 @keyframes fadeIn {
   from { opacity: 0; }

+ 211 - 20
lead-discovery/src/app/pages/dashboard/dashboard.component.ts

@@ -14,21 +14,30 @@ import { MatDividerModule } from '@angular/material/divider';
 import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
 import { MatProgressBarModule } from '@angular/material/progress-bar';
 import { MatBadgeModule } from '@angular/material/badge';
+import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
 import { MockDataService } from '../../services/mock-data.service';
+import { ParseDataService } from '../../services/parse-data.service';
 import { QuickScreenService, BatchScreenResult } from '../../services/quick-screen.service';
 import { BatchInputService } from '../../services/batch-input.service';
 import { EntityResolverService } from '../../services/entity-resolver.service';
-import { ScreenResultCardComponent } from '../../components/screen-result-card/screen-result-card.component';
+import { LeadDetailComponent } from '../lead-detail/lead-detail.component';
 import {
-  Lead, Email, EmailAttachment, QuickScreenResult, BatchInputItem, EntityGroup,
+  Lead, Email, EmailAttachment, BatchInputItem, EntityGroup,
   CustomerPersona, PERSONA_DEFINITIONS, PERSONA_LIST
 } from '../../models/lead.model';
+import PostalMime from 'postal-mime';
 
-// EML 解析 API 响应接口
+// EML 解析 API 响应接口(与 ltc-nanchi / Brainwork 兼容:URL 或 base64 content)
 interface EmlParseAttachment {
   filename: string;
   mimeType: string;
   size: number;
+  /** 预览/下载 URL(解析服务上传后返回) */
+  url?: string;
+  contentUrl?: string;
+  previewUrl?: string;
+  /** base64 内容(若 API 返回则用于生成 blob 预览,与 ltc-nanchi att.content 一致) */
+  content?: string;
 }
 
 interface EmlProductDemand {
@@ -53,6 +62,8 @@ interface EmlParseData {
   toList: { name: string; email: string }[];
   ccList: { name: string; email: string }[];
   cleanedBody: string;
+  /** HTML 正文(若解析服务返回) */
+  htmlBody?: string;
   attachments: EmlParseAttachment[];
   customerEmail: string;
   contactName: string;
@@ -69,6 +80,8 @@ interface EmlParseData {
     skipped: number;
     bodyAnalyzed: boolean;
   };
+  /** 与 ltc-nanchi 一致:解析服务上传后的附件列表(含 url) */
+  attachmentAnalysis?: { attachments: EmlParseAttachment[] };
 }
 
 interface EmlParseResponse {
@@ -87,7 +100,7 @@ interface EmlParseResponse {
         MatChipsModule, MatSnackBarModule, MatTooltipModule,
         MatTabsModule, MatDividerModule,
         MatProgressSpinnerModule, MatProgressBarModule, MatBadgeModule,
-        ScreenResultCardComponent,
+        LeadDetailComponent,
     ],
     templateUrl: './dashboard.component.html',
     styleUrls: ['./dashboard.component.scss'],
@@ -123,7 +136,7 @@ export class DashboardComponent implements OnInit {
   importProgress = 0;
   importStepMessage = '';
   importedEmailId: string | null = null;
-  importScreenResult: QuickScreenResult | null = null;
+  importScreenResult: Lead | null = null;
 
   importData = {
     companyName: '',
@@ -214,12 +227,14 @@ export class DashboardComponent implements OnInit {
 
   constructor(
     private mockData: MockDataService,
+    private parseData: ParseDataService,
     private screenService: QuickScreenService,
     private batchInputService: BatchInputService,
     private entityResolver: EntityResolverService,
     private snackBar: MatSnackBar,
     public router: Router,
     private http: HttpClient,
+    private sanitizer: DomSanitizer,
   ) {}
 
   ngOnInit() {
@@ -376,6 +391,11 @@ export class DashboardComponent implements OnInit {
       const formData = new FormData();
       formData.append('file', file, file.name);
 
+      const fileBuffer = await file.arrayBuffer();
+      const parser = new PostalMime();
+      const localParsed = await parser.parse(fileBuffer);
+      const localAttachments = localParsed.attachments || [];
+
       const response = await this.http.post<EmlParseResponse>(
         `${this.EML_API_URL}/api/parse?analyze_attachments=true`,
         formData,
@@ -392,6 +412,9 @@ export class DashboardComponent implements OnInit {
 
       const data = response.data;
 
+      // 按 DEMO_NANCHI_REUSE_SPEC:构建并保存 Email;用本地解析的附件内容生成预览链接(与 ltc-nanchi 一致)
+      await this.buildAndSaveEmailFromEml(data, localAttachments);
+
       this.importData.companyName = data.companyName || this.extractCompanyFromDomain(data.companyDomain) || '';
       this.importData.contactName = data.contactName || data.senderName || '';
       this.importData.email = data.customerEmail || data.senderEmail || '';
@@ -434,6 +457,101 @@ export class DashboardComponent implements OnInit {
     return name.charAt(0).toUpperCase() + name.slice(1);
   }
 
+  /** 用 base64 生成 blob URL,用于附件预览(blob URL 仅当前会话有效) */
+  private createBlobUrlFromBase64(base64: string, mimeType: string): string {
+    try {
+      const binary = atob(base64.replace(/^data:[^;]+;base64,/, '').trim());
+      const bytes = new Uint8Array(binary.length);
+      for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
+      const blob = new Blob([bytes], { type: mimeType || 'application/octet-stream' });
+      return URL.createObjectURL(blob);
+    } catch {
+      return '';
+    }
+  }
+
+  /** 按 DEMO_NANCHI_REUSE_SPEC 将 EmlParseData 转为 Email;localAttachments 为 postal-mime 本地解析的附件(含 content),用于生成预览 blob URL */
+  private async buildAndSaveEmailFromEml(
+    data: EmlParseData,
+    localAttachments?: Array<{ filename: string | null; mimeType: string; content: ArrayBuffer | string }>
+  ): Promise<Email> {
+    const local = localAttachments ?? [];
+    const id = data.messageId || `eml-${Date.now()}`;
+    const from = data.customerEmail || data.senderEmail || '';
+    const cleanedBody = data.cleanedBody || '';
+    const ts = Date.now();
+
+    const rawAttachments = data.attachmentAnalysis?.attachments ?? data.attachments ?? [];
+    const attachments: EmailAttachment[] = rawAttachments.map((att, idx) => {
+      const a = att as EmlParseAttachment & { contentBase64?: string; data?: string };
+      let url = a.url ?? a.contentUrl ?? a.previewUrl;
+      let base64 = a.content ?? a.contentBase64 ?? a.data;
+      if (!url && typeof base64 === 'string') {
+        url = this.createBlobUrlFromBase64(base64, att.mimeType || 'application/octet-stream');
+      }
+      const fileName = att.filename || (att as { name?: string }).name || `attachment-${idx}`;
+      if (!url && local.length > 0) {
+        const localAtt = local[idx] ?? local.find(la => (la.filename || '').trim() === (fileName || '').trim());
+        if (localAtt && typeof localAtt.content === 'string') {
+          url = this.createBlobUrlFromBase64(localAtt.content, localAtt.mimeType || att.mimeType || 'application/octet-stream');
+        } else if (localAtt && localAtt.content instanceof ArrayBuffer) {
+          try {
+            const blob = new Blob([localAtt.content], { type: localAtt.mimeType || att.mimeType || 'application/octet-stream' });
+            url = URL.createObjectURL(blob);
+          } catch {
+            url = '';
+          }
+        }
+      }
+      return {
+        id: `att-${idx}-${ts}`,
+        fileName,
+        fileSize: att.size || 0,
+        mimeType: att.mimeType || 'application/octet-stream',
+        icon: this.mimeTypeToIcon(att.mimeType || ''),
+        url,
+      };
+    });
+
+    const to = (data.toList && data.toList.length > 0)
+      ? data.toList.map(t => t.email).join(', ')
+      : '';
+    const cc = (data.ccList && data.ccList.length > 0)
+      ? data.ccList.map(t => t.email).join(', ')
+      : undefined;
+
+    const email: Email = {
+      id,
+      from,
+      fromName: data.contactName || data.senderName || '',
+      to,
+      cc,
+      subject: data.subject || '',
+      preview: cleanedBody.slice(0, 200),
+      body: cleanedBody,
+      htmlContent: data.htmlBody,
+      attachments,
+      receivedAt: data.date ? new Date(data.date) : new Date(),
+      hasScreened: false,
+    };
+
+    this.emails.push(email);
+    this.mockData.addEmail(email);
+
+    const parseId = await this.parseData.saveEmail(email);
+    if (parseId) email.parseObjectId = parseId;
+
+    return email;
+  }
+
+  private mimeTypeToIcon(mimeType: string): string {
+    const m = (mimeType || '').toLowerCase();
+    if (m.includes('pdf')) return 'picture_as_pdf';
+    if (m.includes('spreadsheet') || m.includes('vnd.openxmlformats') || m.includes('excel')) return 'table_chart';
+    if (m.includes('word') || m.includes('msword')) return 'description';
+    return 'insert_drive_file';
+  }
+
   clearImport() {
     this.importedEmailId = null;
     this.importData = { companyName: '', contactName: '', roleDescription: '', email: '', website: '' };
@@ -445,31 +563,81 @@ export class DashboardComponent implements OnInit {
   async importAndScreen() {
     this.isScreening = true;
     this.screeningStep = '🤖 AI 分析中...';
-    this.importScreenResult = await this.screenService.screenManual(
+    const screenResult = await this.screenService.screenManual(
       this.importData.companyName,
       this.importData.email,
       (p) => { this.screeningStep = p.message; }
     );
+    // 用 EML 解析出的「提取的产品需求」填充 demandSnapshot,需求摘要展示此项而非模拟数据
+    if (this.importedProducts.length > 0) {
+      const products = this.importedProducts.map(p => ({
+        name: p.productName,
+        quantity: this.parseProductQuantity(p.quantity),
+        unit: '件/年'
+      }));
+      let estimatedAnnualValue = 0;
+      this.importedProducts.forEach(p => {
+        const n = parseFloat((p.totalPrice || '0').replace(/[^0-9.]/g, '')) || 0;
+        if (n > 0) estimatedAnnualValue += n;
+      });
+      screenResult.demandSnapshot = {
+        products,
+        estimatedAnnualValue,
+        certifications: []
+      };
+    }
+    const importedEmail = this.emails.find(e => e.from === this.importData.email);
+    const createResult = this.screenService.createLeadFromScreen(screenResult, importedEmail);
+    const lead = createResult.lead;
+    if (this.importedProducts.length > 0) {
+      lead.extractedProductDemands = this.importedProducts.map(p => ({
+        productName: p.productName,
+        quantity: p.quantity,
+        specifications: p.specifications,
+        source: p.source,
+        totalPrice: p.totalPrice,
+        unitPrice: p.unitPrice,
+        moq: p.moq
+      }));
+    }
+    this.importScreenResult = lead;
     this.isScreening = false;
   }
 
-  onCreateImportLead() {
+  private parseProductQuantity(q: string): number {
+    if (!q) return 0;
+    const num = parseFloat(q.replace(/[^0-9.]/g, ''));
+    return isNaN(num) ? 0 : num;
+  }
+
+  async onCreateImportLead() {
     if (!this.importScreenResult) return;
-    const result = this.screenService.createLeadFromScreen(this.importScreenResult);
+    const lead = this.importScreenResult;
 
-    if (result.isDuplicate) {
-      const existCount = result.existingEntity?.existingLeadIds.length || 0;
-      this.snackBar.open(
-        `⚠ 该客户已有 ${existCount} 条历史记录(${result.matchInfo}),已关联到同一主体`,
-        '查看看板',
-        { duration: 5000 }
-      ).onAction().subscribe(() => this.router.navigate(['/list']));
-    } else {
-      this.snackBar.open(`✅ 线索已创建:${result.lead.companyName}`, '查看看板', { duration: 4000 })
-        .onAction().subscribe(() => this.router.navigate(['/list']));
+    // 保存 Lead 到 Parse
+    const leadParseId = await this.parseData.saveLead(lead);
+    if (leadParseId) lead.id = leadParseId;
+
+    // 按 DEMO_NANCHI_REUSE_SPEC:建立 Lead ↔ Email 关联
+    const email = this.emails.find(e => e.from === this.importData.email);
+    if (email) {
+      email.leadId = lead.id;
+      email.hasScreened = true;
+      email.screenResult = lead.quickScreenResult;
+      if (email.parseObjectId && leadParseId) {
+        await this.parseData.updateEmailLeadId(email.parseObjectId, leadParseId);
+      }
     }
 
-    this.leads.push(result.lead);
+    this.snackBar.open(`✅ 线索已创建:${lead.companyName}`, '查看看板', { duration: 4000 })
+      .onAction().subscribe(() => this.router.navigate(['/list']));
+
+    this.leads.push(lead);
+    this.mockData.addLead(lead);
+    if (!lead.personaVerified) {
+      this.pendingLeads.push(lead);
+      this.pendingCount++;
+    }
     this.clearImport();
   }
 
@@ -637,8 +805,31 @@ export class DashboardComponent implements OnInit {
     return 'att-color-other';
   }
 
+  /** 点击附件:新窗口打开预览(图片用 img、PDF 用 iframe) */
   downloadAttachment(att: EmailAttachment) {
-    this.snackBar.open(`📎 正在下载: ${att.fileName} (${this.formatFileSize(att.fileSize)})`, '', { duration: 2000 });
+    if (att.url) {
+      this.openAttachmentInNewWindow(att.url, att.mimeType);
+    } else {
+      this.snackBar.open(`📎 该附件暂无预览链接: ${att.fileName}`, '', { duration: 2000 });
+    }
+  }
+
+  /** 在新窗口打开附件:用临时 a[target=_blank] 点击打开,新标签直接加载 blob URL 才能正确预览(内嵌 iframe 在 Edge 会黑屏) */
+  private openAttachmentInNewWindow(url: string, _mimeType?: string): void {
+    const a = document.createElement('a');
+    a.href = url;
+    a.target = '_blank';
+    a.rel = 'noopener noreferrer';
+    document.body.appendChild(a);
+    a.click();
+    document.body.removeChild(a);
+  }
+
+  /** 邮件正文预览:有 htmlContent 用 HTML,否则纯文本换行转 <br>;返回 SafeHtml 避免消毒截断(与 ltc-nanchi 一致) */
+  formatEmailContent(email: Email): SafeHtml {
+    const raw = email.htmlContent ?? email.body ?? '';
+    const html = email.htmlContent ? raw : (raw.replace(/\n/g, '<br>'));
+    return this.sanitizer.bypassSecurityTrustHtml(html);
   }
 
   // ===== 批量导入 =====

+ 179 - 170
lead-discovery/src/app/pages/lead-detail/lead-detail.component.html

@@ -1,6 +1,6 @@
-<div class="page-container" *ngIf="lead">
-  <!-- Back -->
-  <a class="back-link" (click)="goBack()">
+<div class="page-container" [class.preview-mode]="isPreview" *ngIf="lead">
+  <!-- Back (page mode only) -->
+  <a class="back-link" (click)="goBack()" *ngIf="!isPreview">
     <mat-icon>arrow_back</mat-icon> 返回上一页
   </a>
 
@@ -21,17 +21,20 @@
             <span class="badge" [class]="'grade-' + lead.valueGrade.toLowerCase()">
               {{ lead.valueGrade }}级
             </span>
-            <span class="badge stage-badge" [class]="'stage-' + lead.followUpStage">
+            <span class="badge stage-badge" [class]="'stage-' + lead.followUpStage" *ngIf="!isPreview">
               {{ getStageLabel(lead.followUpStage) }}
             </span>
-            <span class="verify-badge" [class.verified]="lead.personaVerified">
+            <span class="action-badge" [class]="'action-' + lead.valueGrade.toLowerCase()" *ngIf="isPreview">
+              {{ getActionLabel(lead.valueGrade) }}
+            </span>
+            <span class="verify-badge" [class.verified]="lead.personaVerified" *ngIf="!isPreview">
               <mat-icon>{{ lead.personaVerified ? 'verified' : 'pending' }}</mat-icon>
               {{ lead.personaVerified ? '已验证' : '待验证' }}
             </span>
           </div>
         </div>
       </div>
-      <div class="header-right">
+      <div class="header-right" *ngIf="!isPreview">
         <div class="value-display">
         </div>
         <button
@@ -81,7 +84,7 @@
           <div class="insight-row">
             <span class="insight-label">AI 判断依据</span>
             <div class="insight-keywords">
-              <span class="kw-chip" *ngFor="let kw of lead.quickScreenResult.matchedKeywords">{{ kw }}</span>
+              <span class="kw-chip" *ngFor="let kw of getMatchedKeywords()">{{ kw }}</span>
             </div>
           </div>
 
@@ -196,6 +199,8 @@
           <div class="demand-list">
             <div class="demand-item" *ngFor="let p of getDemandSnapshot()?.products">
               <span class="demand-product">· {{ p.name }} x {{ p.quantity }}{{ p.unit }}</span>
+              <div class="demand-specs" *ngIf="p.specifications">{{ p.specifications }}</div>
+              <div class="demand-source" *ngIf="p.source">来源: {{ p.source }}</div>
             </div>
           </div>
           <div class="estimated-value" *ngIf="getDemandSnapshot()?.estimatedAnnualValue">
@@ -240,7 +245,7 @@
 
         <div class="email-body-section">
           <div class="email-body" [class.collapsed]="!emailBodyExpanded">
-            <pre class="email-body-text">{{ email.body }}</pre>
+            <div class="email-body-text content-html" [innerHTML]="formatEmailContent(email)"></div>
           </div>
           <button mat-button class="expand-btn" (click)="emailBodyExpanded = !emailBodyExpanded">
             <mat-icon>{{ emailBodyExpanded ? 'expand_less' : 'expand_more' }}</mat-icon>
@@ -255,10 +260,12 @@
             <span>附件 ({{ email.attachments.length }})</span>
           </div>
           <div class="attachment-list">
-            <div class="attachment-chip" *ngFor="let att of email.attachments">
+            <div class="attachment-chip attachment-clickable" *ngFor="let att of email.attachments"
+                 (click)="openAttachmentPreview(att)" [title]="att.url ? '点击下载/预览附件' : '该附件暂无预览链接'">
               <mat-icon class="att-icon">{{ att.icon }}</mat-icon>
               <span class="att-name">{{ att.fileName }}</span>
               <span class="att-size">{{ formatFileSize(att.fileSize) }}</span>
+              <mat-icon class="att-open-icon" *ngIf="att.url">open_in_new</mat-icon>
             </div>
           </div>
         </div>
@@ -279,31 +286,6 @@
 
     <!-- Right Column -->
     <div class="right-col">
-      <!-- Product Recommendations -->
-      <div class="section-card card">
-        <div class="section-header">
-          <mat-icon>inventory_2</mat-icon>
-          <h3>推荐产品</h3>
-        </div>
-        <div class="product-list">
-          <div class="product-card" *ngFor="let p of lead.quickScreenResult.suggestedProducts; let i = index">
-            <div class="product-rank" [class]="'rank-' + (i < 3 ? i + 1 : 'other')">{{ i + 1 }}</div>
-            <div class="product-info">
-              <div class="product-name">{{ p.name }}</div>
-              <div class="product-en">{{ p.nameEn }}</div>
-              <div class="product-sku-row">
-                <span class="sku-tag">{{ p.sku }}</span>
-                <span class="product-reason">{{ p.reason }}</span>
-              </div>
-            </div>
-            <div class="product-pricing">
-              <span class="wholesale-price">${{ p.wholesalePrice }}</span>
-              <span class="price-label">批发价</span>
-            </div>
-          </div>
-        </div>
-      </div>
-
       <!-- Credibility + Scores + AI Next Steps (combined into one card) -->
       <div class="section-card card credibility-score-ai-card">
         <!-- Credibility -->
@@ -311,6 +293,18 @@
           <div class="section-header">
             <mat-icon>verified</mat-icon>
             <h3>可信度:{{ getCredibilityLevel() }}</h3>
+            <button
+              *ngIf="needsEnrichment() && !isPreview"
+              mat-stroked-button
+              color="primary"
+              size="small"
+              class="enrich-inline-btn"
+              (click)="runEnrichment()"
+              [disabled]="isEnriching"
+              matTooltip="补充 AI 五维评分与推荐操作">
+              <mat-icon>{{ isEnriching ? 'sync' : 'auto_awesome' }}</mat-icon>
+              {{ isEnriching ? enrichProgress : '补充分析' }}
+            </button>
           </div>
           <div class="credibility-content">
             <div class="credibility-signals">
@@ -420,12 +414,22 @@
             <div class="no-recommendation" *ngIf="!getRecommendedNextStep() && !getRecommendedNextStepsDetails()">
               <mat-icon>info</mat-icon>
               <span>暂无AI推荐操作</span>
+              <button
+                *ngIf="needsEnrichment() && !isPreview"
+                mat-stroked-button
+                color="primary"
+                class="enrich-btn"
+                (click)="runEnrichment()"
+                [disabled]="isEnriching">
+                <mat-icon>{{ isEnriching ? 'sync' : 'auto_awesome' }}</mat-icon>
+                {{ isEnriching ? enrichProgress : 'AI 补充分析' }}
+              </button>
             </div>
           </div>
         </div>
       </div>
 
-      <!-- Sales Verification (NEW) -->
+      <!-- Sales Verification -->
       <div class="section-card card verify-section">
         <div class="section-header">
           <mat-icon>fact_check</mat-icon>
@@ -491,163 +495,168 @@
         </div>
       </div>
 
-      <!-- Deep Research Package (跟进准备包) -->
-      <app-deep-research-package
-        *ngIf="deepResearchResult"
-        [lead]="lead"
-        [researchResult]="deepResearchResult"
-        (editScript)="onEditScript()"
-        (sendEmail)="onSendEmail()"
-        (createLead)="onCreateLead()">
-      </app-deep-research-package>
+      <!-- Deep Research Package (page mode only) -->
+      <ng-container *ngIf="!isPreview">
+        <app-deep-research-package
+          *ngIf="deepResearchResult"
+          [lead]="lead"
+          [researchResult]="deepResearchResult"
+          (editScript)="onEditScript()"
+          (sendEmail)="onSendEmail()"
+          (createLead)="onCreateLead()">
+        </app-deep-research-package>
 
-      <!-- AI Research Report (保留作为备用显示) -->
-      <div class="section-card card" *ngIf="lead.aiResearchReport && !deepResearchResult">
-        <div class="section-header">
-          <mat-icon>analytics</mat-icon>
-          <h3>AI 调研报告</h3>
-          <span class="badge grade-a">已完成</span>
-        </div>
-        <div class="deep-report-preview">
-          <div class="report-item">
-            <mat-icon>business</mat-icon>
-            <div>
-              <strong>官网分析</strong>
-              <p>{{ lead.aiResearchReport.websiteAnalysis }}</p>
+        <!-- AI Research Report -->
+        <div class="section-card card" *ngIf="lead.aiResearchReport && !deepResearchResult">
+          <div class="section-header">
+            <mat-icon>analytics</mat-icon>
+            <h3>AI 调研报告</h3>
+            <span class="badge grade-a">已完成</span>
+          </div>
+          <div class="deep-report-preview">
+            <div class="report-item">
+              <mat-icon>business</mat-icon>
+              <div>
+                <strong>官网分析</strong>
+                <p>{{ lead.aiResearchReport.websiteAnalysis }}</p>
+              </div>
             </div>
-          </div>
-          <div class="report-item" *ngIf="lead.aiResearchReport.productMatching.length">
-            <mat-icon>shopping_cart</mat-icon>
-            <div>
-              <strong>产品匹配</strong>
-              <div class="match-list">
-                <div class="match-item" *ngFor="let m of lead.aiResearchReport.productMatching">
-                  <span>{{ m.ourProduct }} ({{ m.ourSku }}) → {{ m.matchesNeed }}</span>
-                  <span class="match-margin">{{ m.profitMargin }}</span>
+            <div class="report-item" *ngIf="lead.aiResearchReport.productMatching.length">
+              <mat-icon>shopping_cart</mat-icon>
+              <div>
+                <strong>产品匹配</strong>
+                <div class="match-list">
+                  <div class="match-item" *ngFor="let m of lead.aiResearchReport.productMatching">
+                    <span>{{ m.ourProduct }} ({{ m.ourSku }}) → {{ m.matchesNeed }}</span>
+                    <span class="match-margin">{{ m.profitMargin }}</span>
+                  </div>
                 </div>
               </div>
             </div>
-          </div>
-          <div class="report-item">
-            <mat-icon>groups</mat-icon>
-            <div>
-              <strong>竞品分析</strong>
-              <p>{{ lead.aiResearchReport.competitorAnalysis }}</p>
+            <div class="report-item">
+              <mat-icon>groups</mat-icon>
+              <div>
+                <strong>竞品分析</strong>
+                <p>{{ lead.aiResearchReport.competitorAnalysis }}</p>
+              </div>
             </div>
           </div>
         </div>
-      </div>
 
-      <!-- Background Check Progress -->
-      <div class="section-card card" *ngIf="isBgChecking">
-        <div class="section-header">
-          <mat-icon>sync</mat-icon>
-          <h3>背调进行中</h3>
+        <!-- Background Check Progress -->
+        <div class="section-card card" *ngIf="isBgChecking">
+          <div class="section-header">
+            <mat-icon>sync</mat-icon>
+            <h3>背调进行中</h3>
+          </div>
+          <app-bgcheck-progress
+            [stages]="bgCheckStages"
+            [overallProgress]="bgCheckProgress">
+          </app-bgcheck-progress>
         </div>
-        <app-bgcheck-progress
-          [stages]="bgCheckStages"
-          [overallProgress]="bgCheckProgress">
-        </app-bgcheck-progress>
-      </div>
 
-      <!-- Background Check Result -->
-      <div class="section-card card" *ngIf="lead.hasBackgroundCheck && !isBgChecking">
-        <div class="section-header">
-          <mat-icon>assessment</mat-icon>
-          <h3>背调报告</h3>
-          <span class="badge grade-a">已完成</span>
-        </div>
-        <div class="bgcheck-summary">
-          <div class="summary-item">
-            <span class="summary-label">完成时间</span>
-            <span class="summary-value">{{ lead.backgroundCheckAt | date:'yyyy-MM-dd HH:mm' }}</span>
-          </div>
-          <div class="summary-item" *ngIf="lead.backgroundCheckReport">
-            <span class="summary-label">综合评级</span>
-            <span class="summary-value badge" [class]="'level-' + lead.backgroundCheckReport.rating.level.toLowerCase()">
-              {{ lead.backgroundCheckReport.rating.level }}级 ({{ lead.backgroundCheckReport.rating.overallScore }}分)
-            </span>
-          </div>
-          <div class="summary-item" *ngIf="lead.backgroundCheckReport">
-            <span class="summary-label">风险等级</span>
-            <span class="summary-value" [class]="'risk-' + lead.backgroundCheckReport.risk.level">
-              {{ lead.backgroundCheckReport.risk.level === 'low' ? '低' : lead.backgroundCheckReport.risk.level === 'medium' ? '中' : '高' }}风险
-            </span>
+        <!-- Background Check Result -->
+        <div class="section-card card" *ngIf="lead.hasBackgroundCheck && !isBgChecking">
+          <div class="section-header">
+            <mat-icon>assessment</mat-icon>
+            <h3>背调报告</h3>
+            <span class="badge grade-a">已完成</span>
+          </div>
+          <div class="bgcheck-summary">
+            <div class="summary-item">
+              <span class="summary-label">完成时间</span>
+              <span class="summary-value">{{ lead.backgroundCheckAt | date:'yyyy-MM-dd HH:mm' }}</span>
+            </div>
+            <div class="summary-item" *ngIf="lead.backgroundCheckReport">
+              <span class="summary-label">综合评级</span>
+              <span class="summary-value badge" [class]="'level-' + lead.backgroundCheckReport.rating.level.toLowerCase()">
+                {{ lead.backgroundCheckReport.rating.level }}级 ({{ lead.backgroundCheckReport.rating.overallScore }}分)
+              </span>
+            </div>
+            <div class="summary-item" *ngIf="lead.backgroundCheckReport">
+              <span class="summary-label">风险等级</span>
+              <span class="summary-value" [class]="'risk-' + lead.backgroundCheckReport.risk.level">
+                {{ lead.backgroundCheckReport.risk.level === 'low' ? '低' : lead.backgroundCheckReport.risk.level === 'medium' ? '中' : '高' }}风险
+              </span>
+            </div>
           </div>
+          <button mat-raised-button color="primary" (click)="viewBgCheckReport()">
+            <mat-icon>description</mat-icon> 查看完整报告
+          </button>
         </div>
-        <button mat-raised-button color="primary" (click)="viewBgCheckReport()">
-          <mat-icon>description</mat-icon> 查看完整报告
-        </button>
-      </div>
 
-      <!-- Deep Analysis Placeholder -->
-      <div class="section-card card deep-analysis-placeholder" *ngIf="!lead.hasBackgroundCheck && !lead.aiResearchReport && !deepResearchResult && !isBgChecking"
-           [class.invalid-lead-placeholder]="isInvalidLead()">
-        <div class="placeholder-content">
-          <!-- 无效线索:不建议背调提示 -->
-          <ng-container *ngIf="isInvalidLead(); else normalPlaceholder">
-            <mat-icon class="placeholder-icon warn-icon">do_not_disturb</mat-icon>
-            <h3>AI 不建议进行深度背调</h3>
-            <div class="invalid-lead-notice">
-              <div class="notice-box">
-                <mat-icon>report_problem</mat-icon>
-                <div>
-                  <strong>该线索已被AI判定为低质量/无效线索(P6)</strong>
-                  <p>AI 分析认为该线索不值得投入背调资源。但 AI 分析可能不准确,如果您认为该线索有价值,仍可手动启动深度分析。</p>
+        <!-- Deep Analysis Placeholder -->
+        <div class="section-card card deep-analysis-placeholder" *ngIf="!lead.hasBackgroundCheck && !lead.aiResearchReport && !deepResearchResult && !isBgChecking"
+             [class.invalid-lead-placeholder]="isInvalidLead()">
+          <div class="placeholder-content">
+            <ng-container *ngIf="isInvalidLead(); else normalPlaceholder">
+              <mat-icon class="placeholder-icon warn-icon">do_not_disturb</mat-icon>
+              <h3>AI 不建议进行深度背调</h3>
+              <div class="invalid-lead-notice">
+                <div class="notice-box">
+                  <mat-icon>report_problem</mat-icon>
+                  <div>
+                    <strong>该线索已被AI判定为低质量/无效线索(P6)</strong>
+                    <p>AI 分析认为该线索不值得投入背调资源。但 AI 分析可能不准确,如果您认为该线索有价值,仍可手动启动深度分析。</p>
+                  </div>
                 </div>
               </div>
-            </div>
-            <p class="estimate">预估耗时:3-5 分钟</p>
-            <button
-              mat-raised-button
-              color="warn"
-              (click)="startDeepAnalysis()"
-              [disabled]="!lead.personaVerified"
-              [matTooltip]="!lead.personaVerified ? '请先完成销售验证' : ''">
-              <mat-icon>play_arrow</mat-icon> 仍然启动深度分析
-            </button>
-            <p class="invalid-hint" *ngIf="lead.personaVerified">
-              <mat-icon>info</mat-icon>
-              AI 判断仅供参考,您可以根据实际情况决定是否进行背调
-            </p>
-            <p class="verify-hint" *ngIf="!lead.personaVerified" style="margin-top: 12px; font-size: 13px; color: #ff9800; text-align: center;">
-              <mat-icon style="font-size: 16px; width: 16px; height: 16px; vertical-align: middle;">info</mat-icon>
-              请先完成上方的"销售验证",确认画像分类和价值等级后再启动深度分析
-            </p>
-          </ng-container>
+              <p class="estimate">预估耗时:3-5 分钟</p>
+              <button
+                mat-raised-button
+                color="warn"
+                (click)="startDeepAnalysis()"
+                [disabled]="!lead.personaVerified"
+                [matTooltip]="!lead.personaVerified ? '请先完成销售验证' : ''">
+                <mat-icon>play_arrow</mat-icon> 仍然启动深度分析
+              </button>
+              <p class="invalid-hint" *ngIf="lead.personaVerified">
+                <mat-icon>info</mat-icon>
+                AI 判断仅供参考,您可以根据实际情况决定是否进行背调
+              </p>
+              <p class="verify-hint" *ngIf="!lead.personaVerified" style="margin-top: 12px; font-size: 13px; color: #ff9800; text-align: center;">
+                <mat-icon style="font-size: 16px; width: 16px; height: 16px; vertical-align: middle;">info</mat-icon>
+                请先完成上方的"销售验证",确认画像分类和价值等级后再启动深度分析
+              </p>
+            </ng-container>
 
-          <!-- 正常线索:原有内容 -->
-          <ng-template #normalPlaceholder>
-            <mat-icon class="placeholder-icon">search</mat-icon>
-            <h3>AI 调研报告</h3>
-            <p>启动深度分析后,AI 将根据画像类型执行差异化调研:</p>
-            <ul>
-              <li>🌐 {{ getPersonaStrategyLabel(lead.persona) }}</li>
-              <li>📦 产品匹配与利润分析</li>
-              <li>🏭 竞品与供应链分析</li>
-              <li>📋 销售策略与话术生成</li>
-            </ul>
-            <p class="estimate">预估耗时:3-5 分钟</p>
-            <button
-              mat-raised-button
-              color="primary"
-              (click)="startDeepAnalysis()"
-              [disabled]="!lead.personaVerified"
-              [matTooltip]="!lead.personaVerified ? '请先完成销售验证' : ''">
-              <mat-icon>play_arrow</mat-icon> 启动深度分析
-            </button>
-            <p class="verify-hint" *ngIf="!lead.personaVerified" style="margin-top: 12px; font-size: 13px; color: #ff9800; text-align: center;">
-              <mat-icon style="font-size: 16px; width: 16px; height: 16px; vertical-align: middle;">info</mat-icon>
-              请先完成上方的"销售验证",确认画像分类和价值等级后再启动深度分析
-            </p>
-          </ng-template>
+            <ng-template #normalPlaceholder>
+              <mat-icon class="placeholder-icon">search</mat-icon>
+              <h3>AI 调研报告</h3>
+              <p>启动深度分析后,AI 将根据画像类型执行差异化调研:</p>
+              <ul>
+                <li>🌐 {{ getPersonaStrategyLabel(lead.persona) }}</li>
+                <li>📦 产品匹配与利润分析</li>
+                <li>🏭 竞品与供应链分析</li>
+                <li>📋 销售策略与话术生成</li>
+              </ul>
+              <p class="estimate">预估耗时:3-5 分钟</p>
+              <button
+                mat-raised-button
+                color="primary"
+                (click)="startDeepAnalysis()"
+                [disabled]="!lead.personaVerified"
+                [matTooltip]="!lead.personaVerified ? '请先完成销售验证' : ''">
+                <mat-icon>play_arrow</mat-icon> 启动深度分析
+              </button>
+              <p class="verify-hint" *ngIf="!lead.personaVerified" style="margin-top: 12px; font-size: 13px; color: #ff9800; text-align: center;">
+                <mat-icon style="font-size: 16px; width: 16px; height: 16px; vertical-align: middle;">info</mat-icon>
+                请先完成上方的"销售验证",确认画像分类和价值等级后再启动深度分析
+              </p>
+            </ng-template>
+          </div>
         </div>
-      </div>
+      </ng-container>
     </div>
   </div>
+
+  <!-- Preview Mode: Processing Time (no 确认并建档 button) -->
+  <div class="preview-processing-time" *ngIf="isPreview && lead.quickScreenResult?.processingTime">
+    ⚡ 处理耗时:{{ lead.quickScreenResult.processingTime / 1000 | number:'1.1-1' }}秒
+  </div>
 </div>
 
-<div class="page-container" *ngIf="!lead">
+<div class="page-container" *ngIf="!lead && !isPreview">
   <div class="empty-state card">
     <mat-icon>search_off</mat-icon>
     <p>未找到该线索</p>

+ 34 - 1
lead-discovery/src/app/pages/lead-detail/lead-detail.component.scss

@@ -53,6 +53,7 @@
 .section-header { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; }
 .section-header mat-icon { color: #1976d2; }
 .section-header h3 { margin: 0; font-size: 16px; flex: 1; }
+.enrich-inline-btn { margin-left: auto; }
 .time-badge { background: #e3f2fd; color: #1976d2; padding: 2px 8px; border-radius: 8px; font-size: 11px; font-weight: 600; }
 .confidence-badge { background: #e8f5e9; color: #2e7d32; padding: 2px 8px; border-radius: 8px; font-size: 11px; font-weight: 600; }
 
@@ -109,10 +110,11 @@
 .email-body { overflow: hidden; transition: max-height 0.3s ease; }
 .email-body.collapsed { max-height: 120px; }
 .email-body-text {
-  font-size: 13px; color: #555; line-height: 1.7; white-space: pre-wrap;
+  font-size: 13px; color: #555; line-height: 1.7;
   word-wrap: break-word; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
   margin: 0; padding: 0;
 }
+.email-body-text.content-html img { max-width: 100%; height: auto; }
 .expand-btn { display: flex; align-items: center; gap: 4px; margin: 4px auto 0; font-size: 13px; color: #1976d2; }
 
 .attachments-header { display: flex; align-items: center; gap: 6px; font-size: 13px; color: #666; margin-bottom: 10px; }
@@ -122,10 +124,12 @@
   display: flex; align-items: center; gap: 8px; padding: 8px 12px;
   background: #f8f9fa; border-radius: 8px; border: 1px solid #eee; transition: background 0.15s;
 }
+.attachment-chip.attachment-clickable { cursor: pointer; }
 .attachment-chip:hover { background: #e3f2fd; border-color: #bbdefb; }
 .att-icon { font-size: 20px; width: 20px; height: 20px; color: #e53935; flex-shrink: 0; }
 .att-name { font-size: 13px; color: #333; font-weight: 500; flex: 1; word-break: break-all; }
 .att-size { font-size: 11px; color: #999; flex-shrink: 0; }
+.att-open-icon { font-size: 16px; width: 16px; height: 16px; color: #1976d2; margin-left: 4px; }
 
 .no-email-card { border-left: 4px solid #e0e0e0; }
 .no-email-placeholder {
@@ -228,6 +232,8 @@
 .demand-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
 .demand-item { font-size: 14px; color: #333; }
 .demand-product { }
+.demand-specs { font-size: 12px; color: #666; margin-top: 4px; margin-left: 1em; }
+.demand-source { font-size: 12px; color: #999; margin-top: 2px; margin-left: 1em; }
 .estimated-value {
   display: flex; align-items: center; gap: 6px;
   font-size: 15px; font-weight: 600; color: #2e7d32;
@@ -358,6 +364,7 @@
 .no-recommendation {
   display: flex;
   align-items: center;
+  flex-wrap: wrap;
   gap: 8px;
   padding: 20px;
   color: #999;
@@ -367,6 +374,7 @@
 .no-recommendation mat-icon {
   color: #ccc;
 }
+.enrich-btn { margin-left: 8px; }
 
 /* Invalid Lead Warning - AI Next Steps */
 .next-step-invalid-warning {
@@ -894,5 +902,30 @@
   strong { color: #047857; margin-right: 4px; }
 }
 
+/* Preview Mode */
+.preview-mode {
+  animation: fadeInUp 0.4s ease;
+}
+@keyframes fadeInUp {
+  from { opacity: 0; transform: translateY(16px); }
+  to { opacity: 1; transform: translateY(0); }
+}
+.action-badge {
+  display: inline-flex; align-items: center; gap: 4px;
+  padding: 3px 10px; border-radius: 20px; font-size: 12px; font-weight: 600;
+}
+.action-s { background: #ffebee; color: #c62828; }
+.action-a { background: #fff3e0; color: #e65100; }
+.action-b { background: #fffde7; color: #f9a825; }
+.action-c { background: #fafafa; color: #757575; }
+
+.preview-processing-time {
+  margin-top: 16px;
+  padding-top: 16px;
+  border-top: 1px solid #e0e0e0;
+  font-size: 12px;
+  color: #999;
+}
+
 @media (max-width: 900px) { .content-grid { grid-template-columns: 1fr; } }
 

+ 181 - 43
lead-discovery/src/app/pages/lead-detail/lead-detail.component.ts

@@ -1,4 +1,4 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
@@ -16,9 +16,11 @@ import { MatInputModule } from '@angular/material/input';
 import { MatSelectModule } from '@angular/material/select';
 import { MatExpansionModule } from '@angular/material/expansion';
 import { MatTooltipModule } from '@angular/material/tooltip';
+import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
 import { MockDataService } from '../../services/mock-data.service';
+import { LeadDetailAnalysisService } from '../../services/lead-detail-analysis.service';
 import {
-  Lead, Email, CATEGORY_ICONS, GRADE_LABELS, Product,
+  Lead, Email, EmailAttachment, CATEGORY_ICONS, GRADE_LABELS, Product,
   CustomerPersona, PERSONA_DEFINITIONS, PERSONA_LIST, ValueGrade
 } from '../../models/lead.model';
 import { DeepResearchPackageComponent, DeepResearchResult } from '../../components/deep-research-package/deep-research-package.component';
@@ -39,7 +41,13 @@ import { BgCheckProgressComponent, BgCheckStage } from '../../components/bgcheck
     styleUrls: ['./lead-detail.component.scss'],
 })
 export class LeadDetailComponent implements OnInit {
-  lead: Lead | null = null;
+  @Input() lead: Lead | null = null;
+  @Input() mode: 'page' | 'preview' = 'page';
+  @Output() createLead = new EventEmitter<void>();
+  @Output() dismiss = new EventEmitter<void>();
+
+  get isPreview(): boolean { return this.mode === 'preview'; }
+
   personaOptions = PERSONA_LIST;
   correctedPersona: CustomerPersona | null = null;
   correctedGrade: ValueGrade | null = null;
@@ -53,13 +61,17 @@ export class LeadDetailComponent implements OnInit {
   bgCheckProgress = 0;
 
   private emails: Email[] = [];
+  isEnriching = false;
+  enrichProgress = '';
 
   constructor(
     private route: ActivatedRoute,
     private router: Router,
     private location: Location,
     private mockData: MockDataService,
+    private analysisService: LeadDetailAnalysisService,
     private snackBar: MatSnackBar,
+    private sanitizer: DomSanitizer,
   ) {
     this.bgCheckStages = [
       { id: 1, name: '初始化数据通过', status: 'waiting', icon: 'schedule', description: '验证输入信息' },
@@ -70,11 +82,20 @@ export class LeadDetailComponent implements OnInit {
     ];
   }
 
-  ngOnInit() {
-    const id = this.route.snapshot.paramMap.get('id');
-    this.lead = this.mockData.leads.find(l => l.id === id) || null;
-    this.emails = this.mockData.emails;
+  async ngOnInit() {
+    if (!this.lead) {
+      const id = this.route.snapshot.paramMap.get('id');
+      this.lead = this.mockData.allLeads.find(l => l.id === id) || null;
+    }
+    this.emails = [...this.mockData.allEmails];
     this.deepResearchResult = this.getDeepResearchResult(this.lead);
+
+    if (this.lead) {
+      const analysis = await this.analysisService.loadAnalysisForLead(this.lead, this.emails);
+      if (analysis.email && analysis.emailFromParse) {
+        this.emails = [...this.emails, analysis.email];
+      }
+    }
   }
 
   getPersonaColor(p: CustomerPersona): string { return PERSONA_DEFINITIONS[p]?.color || '#999'; }
@@ -89,6 +110,7 @@ export class LeadDetailComponent implements OnInit {
   }
 
   getCatIcon(cat: string): string { return CATEGORY_ICONS[cat as keyof typeof CATEGORY_ICONS] || 'business'; }
+  getActionLabel(grade: string): string { return GRADE_LABELS[grade as keyof typeof GRADE_LABELS] || ''; }
 
   getStageLabel(stage: string): string {
     const labels: Record<string, string> = { screened: '已筛选', contacted: '已触达', replied: '已回复', sample: '寄样中', quote: '报价中', order: '已下单' };
@@ -101,7 +123,8 @@ export class LeadDetailComponent implements OnInit {
   }
 
   getEmailForLead(lead: Lead): Email | undefined {
-    return this.emails.find(e => e.from === lead.contactEmail);
+    return this.emails.find(e => e.leadId === lead.id)
+      ?? this.emails.find(e => e.from === lead.contactEmail);
   }
 
   formatFileSize(bytes: number): string {
@@ -110,6 +133,33 @@ export class LeadDetailComponent implements OnInit {
     return (bytes / 1048576).toFixed(1) + ' MB';
   }
 
+  /** 点击附件:有 url 则新窗口打开预览(图片用 img、PDF 用 iframe),否则提示 */
+  openAttachmentPreview(att: EmailAttachment): void {
+    if (att.url) {
+      this.openAttachmentInNewWindow(att.url, att.mimeType);
+    } else {
+      this.snackBar.open('该附件暂无预览链接', '', { duration: 2000 });
+    }
+  }
+
+  /** 在新窗口打开附件:用临时 a[target=_blank] 点击打开,新标签直接加载 blob URL 才能正确预览(内嵌 iframe 在 Edge 会黑屏) */
+  private openAttachmentInNewWindow(url: string, _mimeType?: string): void {
+    const a = document.createElement('a');
+    a.href = url;
+    a.target = '_blank';
+    a.rel = 'noopener noreferrer';
+    document.body.appendChild(a);
+    a.click();
+    document.body.removeChild(a);
+  }
+
+  /** 邮件正文预览:有 htmlContent 用 HTML,否则纯文本换行转 <br>;返回 SafeHtml 避免 Angular 消毒截断(与 ltc-nanchi 一致) */
+  formatEmailContent(email: Email): SafeHtml {
+    const raw = email.htmlContent ?? email.body ?? '';
+    const html = email.htmlContent ? raw : (raw.replace(/\n/g, '<br>'));
+    return this.sanitizer.bypassSecurityTrustHtml(html);
+  }
+
   confirmPersona() {
     this.snackBar.open('✅ 画像分类已确认', '', { duration: 1500 });
   }
@@ -128,6 +178,7 @@ export class LeadDetailComponent implements OnInit {
       feedbackAt: new Date(),
     };
     this.snackBar.open(`✅ 已确认:${this.lead.companyName} → ${this.lead.personaLabel}`, '', { duration: 2000 });
+    if (this.isPreview) this.createLead.emit();
   }
 
   editLead() {
@@ -136,6 +187,11 @@ export class LeadDetailComponent implements OnInit {
 
   dismissLead() {
     if (!this.lead) return;
+    if (this.isPreview) {
+      this.dismiss.emit();
+      this.snackBar.open(`❌ 已放弃:${this.lead.companyName}`, '', { duration: 2000 });
+      return;
+    }
     this.lead.personaVerified = true;
     this.lead.persona = 'P6' as CustomerPersona;
     this.lead.personaLabel = '低质量/无效线索';
@@ -168,6 +224,7 @@ export class LeadDetailComponent implements OnInit {
       feedbackAt: new Date(),
     };
     this.snackBar.open('💾 验证结果已保存', '', { duration: 2000 });
+    if (this.isPreview) this.createLead.emit();
   }
 
   goBack() {
@@ -325,70 +382,107 @@ export class LeadDetailComponent implements OnInit {
     }
   }
 
-  getDemandSnapshot(): { products: Array<{ name: string; quantity: number; unit: string }>; estimatedAnnualValue: number } | undefined {
+  /** 需求摘要:优先展示提取的产品需求(extractedProductDemands / demandSnapshot),不做模拟推断 */
+  getDemandSnapshot(): {
+    products: Array<{ name: string; quantity: number; unit: string; specifications?: string; source?: string }>;
+    estimatedAnnualValue: number;
+  } | undefined {
     if (!this.lead) return undefined;
-
-    if (this.lead.quickScreenResult.suggestedProducts.length > 0) {
+    const extracted = this.lead.extractedProductDemands;
+    if (extracted?.length) {
+      let estimated = 0;
+      extracted.forEach(p => {
+        const n = parseFloat((p.totalPrice || '0').replace(/[^0-9.]/g, '')) || 0;
+        if (n > 0) estimated += n;
+      });
       return {
-        products: this.lead.quickScreenResult.suggestedProducts.map(p => ({
-          name: p.name,
-          quantity: 1000,
-          unit: '/年'
+        products: extracted.map(p => ({
+          name: p.productName,
+          quantity: this.parseDemandQuantity(p.quantity),
+          unit: '件/年',
+          specifications: p.specifications,
+          source: p.source
         })),
-        estimatedAnnualValue: this.lead.estimatedOrderValue ||
-          this.lead.quickScreenResult.suggestedProducts.reduce((sum, p) => sum + p.wholesalePrice * 1000, 0)
+        estimatedAnnualValue: estimated || (this.lead.estimatedOrderValue ?? 0)
       };
     }
-    return undefined;
+    const qsr = this.lead.quickScreenResult;
+    if (!qsr.demandSnapshot?.products?.length) return undefined;
+    return {
+      products: qsr.demandSnapshot.products.map(p => ({
+        name: p.name,
+        quantity: p.quantity,
+        unit: p.unit ?? '件/年'
+      })),
+      estimatedAnnualValue: qsr.demandSnapshot.estimatedAnnualValue ?? this.lead.estimatedOrderValue ?? 0
+    };
+  }
+
+  private parseDemandQuantity(q: string): number {
+    if (!q) return 0;
+    const num = parseFloat(q.replace(/[^0-9.]/g, ''));
+    return isNaN(num) ? 0 : num;
   }
 
   getCredibilityLevel(): 'high' | 'medium' | 'low' {
     if (!this.lead) return 'medium';
+    const qsr = this.lead.quickScreenResult;
+
+    // 优先使用 AI 分析的 credibilityDetails(来自 Enterprise.verificationInfo / AgentValueAnalysis)
+    if (qsr.credibilityDetails?.level) {
+      const l = String(qsr.credibilityDetails.level).toLowerCase();
+      if (l === 'high' || l === 'medium' || l === 'low') return l as 'high' | 'medium' | 'low';
+    }
 
-    const domain = this.lead.quickScreenResult.domain;
+    const domain = qsr.domain;
     if (domain && !domain.includes('gmail') && !domain.includes('hotmail') && !domain.includes('yahoo')) {
       return 'high';
     }
-    if (this.lead.quickScreenResult.confidence > 70) {
-      return 'medium';
-    }
+    if (qsr.confidence > 70) return 'medium';
     return 'low';
   }
 
   getCredibilitySignals(): string[] {
     if (!this.lead) return ['基础信息完整'];
+    const qsr = this.lead.quickScreenResult;
 
-    const signals: string[] = [];
-    const domain = this.lead.quickScreenResult.domain;
+    // 优先使用 AI 分析的 credibilityDetails.signals
+    if (qsr.credibilityDetails?.signals?.length) {
+      return qsr.credibilityDetails.signals;
+    }
 
+    const signals: string[] = [];
+    const domain = qsr.domain;
     if (domain && !domain.includes('gmail') && !domain.includes('hotmail') && !domain.includes('yahoo')) {
       signals.push('企业邮箱');
     }
-    if (this.lead.quickScreenResult.confidence > 70) {
-      signals.push('完整签名块');
-    }
-    if (this.lead.quickScreenResult.certRequirements.length > 0) {
-      signals.push('有认证要求');
-    }
-    if (this.lead.contactName && this.lead.contactName.length > 0) {
-      signals.push('联系人信息完整');
-    }
-
+    if (qsr.confidence > 70) signals.push('完整签名块');
+    if ((qsr.certRequirements?.length ?? 0) > 0) signals.push('有认证要求');
+    if (this.lead.contactName?.length) signals.push('联系人信息完整');
     return signals.length > 0 ? signals : ['基础信息完整'];
   }
 
   getScores(): { demandClarity: number; credibility: number; businessScale: number; productFit: number } | undefined {
     if (!this.lead) return undefined;
+    const qsr = this.lead.quickScreenResult;
+
+    // 优先使用 AI 分析的五维评分(来自 AgentValueAnalysisService / EmailAnalysis)
+    if (qsr.scores) {
+      return {
+        demandClarity: qsr.scores.demandClarity ?? 60,
+        credibility: qsr.scores.credibility ?? 70,
+        businessScale: qsr.scores.businessScale ?? 70,
+        productFit: qsr.scores.productFit ?? 70,
+      };
+    }
 
     const gradeScores: Record<string, number> = { S: 90, A: 80, B: 70, C: 60 };
     const baseScore = gradeScores[this.lead.valueGrade] || 70;
-
     return {
-      demandClarity: this.lead.quickScreenResult.suggestedProducts.length > 0 ? 85 : 60,
+      demandClarity: (qsr.suggestedProducts?.length ?? 0) > 0 ? 85 : 60,
       credibility: this.getCredibilityLevel() === 'high' ? 85 : this.getCredibilityLevel() === 'medium' ? 70 : 55,
       businessScale: baseScore,
-      productFit: this.lead.quickScreenResult.productRelevance === 'high' ? 85 :
-                   this.lead.quickScreenResult.productRelevance === 'medium' ? 70 : 55
+      productFit: qsr.productRelevance === 'high' ? 85 : qsr.productRelevance === 'medium' ? 70 : 55
     };
   }
 
@@ -404,6 +498,7 @@ export class LeadDetailComponent implements OnInit {
   getRecommendedNextStep(): string {
     if (!this.lead) return '';
 
+    // 优先 recommendedAction(来自 EmailAnalysis.nextSteps / AgentValueAnalysis)
     if (this.lead.quickScreenResult?.recommendedAction) {
       return this.lead.quickScreenResult.recommendedAction;
     }
@@ -411,15 +506,58 @@ export class LeadDetailComponent implements OnInit {
     if (this.lead.persona) {
       return this.getPersonaFirstAction(this.lead.persona);
     }
-
     return '';
   }
 
   getRecommendedNextStepsDetails(): { timeframe?: string; filesToSend?: string[]; keyPoints?: string[] } | null {
-    if (!this.lead?.quickScreenResult?.recommendedActions) {
-      return null;
+    return this.lead?.quickScreenResult?.recommendedActions ?? null;
+  }
+
+  /** 客户画像:AI 判断依据,无时用画像定义的关键词作为参考 */
+  getMatchedKeywords(): string[] {
+    if (!this.lead) return [];
+    const kw = this.lead.quickScreenResult?.matchedKeywords;
+    if (kw?.length) return kw;
+    const def = PERSONA_DEFINITIONS[this.lead.persona];
+    return def?.identifyKeywords?.slice(0, 5) ?? [];
+  }
+
+  /** 是否需要 AI 补充分析(缺少 scores/demandSnapshot/credibilityDetails) */
+  needsEnrichment(): boolean {
+    if (!this.lead || this.isPreview) return false;
+    const qsr = this.lead.quickScreenResult;
+    const hasScores = qsr?.scores != null;
+    const hasDemand =
+      (this.lead.extractedProductDemands?.length ?? 0) > 0 ||
+      (qsr?.demandSnapshot?.products?.length ?? 0) > 0;
+    const hasCredibility = qsr?.credibilityDetails != null;
+    return !hasScores || !hasDemand || !hasCredibility;
+  }
+
+  async runEnrichment() {
+    if (!this.lead || this.isEnriching) return;
+    this.isEnriching = true;
+    this.enrichProgress = '分析中...';
+    try {
+      const enriched = await this.analysisService.enrichAnalysis(
+        this.lead,
+        (msg) => { this.enrichProgress = msg; }
+      );
+      if (enriched) {
+        this.lead.quickScreenResult = this.analysisService.mergeEnrichedResult(
+          this.lead.quickScreenResult,
+          enriched,
+        );
+        this.snackBar.open('✅ AI 补充分析完成', '', { duration: 2000 });
+      } else {
+        this.snackBar.open('ℹ️ 已有完整分析数据,无需补充', '', { duration: 2000 });
+      }
+    } catch (err) {
+      this.snackBar.open('❌ AI 补充分析失败,请检查 Token 配置', '', { duration: 3000 });
+    } finally {
+      this.isEnriching = false;
+      this.enrichProgress = '';
     }
-    return this.lead.quickScreenResult.recommendedActions;
   }
 
   // ===== Quotation Generation =====
@@ -595,7 +733,7 @@ export class LeadDetailComponent implements OnInit {
       companyInfo: {
         name: '南驰(广州)医疗科技有限公司',
         address: '广东省广州市黄埔区科学城',
-        website: 'www.rhinorescue.com',
+        website: 'www.nanchi.com',
         phone: '+86-020-XXXXXXXX',
       },
     };

+ 1 - 1
lead-discovery/src/app/pages/list/list.component.ts

@@ -78,7 +78,7 @@ export class ListComponent implements OnInit {
   ) {}
 
   ngOnInit() {
-    this.leads = [...this.mockData.leads];
+    this.leads = [...this.mockData.allLeads];
     this.filteredLeads = this.leads;
     this.computeStats();
     this.buildChartStats();

+ 1 - 1
lead-discovery/src/app/pages/product-catalog/product-catalog.component.ts

@@ -22,7 +22,7 @@ import { Product, CATEGORY_LABELS, CustomerCategory } from '../../models/lead.mo
     <div class="page-container">
       <div class="page-header">
         <h1><mat-icon>inventory_2</mat-icon> 产品目录</h1>
-        <p class="subtitle">犀牛急救产品线概览 · 来自价盘 4.1({{ products.length }} 个核心 SKU)</p>
+        <p class="subtitle">南驰产品线概览 · 来自价盘 4.1({{ products.length }} 个核心 SKU)</p>
       </div>
 
       <!-- Filter -->

+ 26 - 14
lead-discovery/src/app/services/ai-screen.service.ts

@@ -82,11 +82,13 @@ const FREE_EMAIL_PROVIDERS = [
   'qq.com', '163.com', '126.com', 'foxmail.com', 'mail.com',
 ];
 
+/** 客户画像/需求/推荐产品用 fmode-1.6-cn;可信度/评分用 gemini-2.5-flash(有联网数据时) */
+const MODEL_PERSONA = 'fmode-1.6-cn';
+const MODEL_CREDIBILITY = 'gemini-2.5-flash';
+
 @Injectable({ providedIn: 'root' })
 export class AiScreenService {
 
-  private readonly MODEL_NAME = 'fmode-1.6-cn';
-
   constructor(
     private generateApi: GenerateApiService,
     private mockData: MockDataService,
@@ -118,22 +120,32 @@ export class AiScreenService {
       console.warn('[AiScreen] 快速背调失败,仅用线索信息:', err);
     }
 
-    // Phase 2: AI 分类(使用 fmode-ng completionJSON)
-    onProgress?.({ progress: 55, message: '🤖 AI 正在分析线索价值...' });
+    // Phase 2: AI 分析
+    // 有联网背调数据且 Token 可用时,用 gemini-2.5-flash 提升可信度/评分准确度(参考 lead-detail-reuse-mapping.md)
+    const useGemini = bgContext && this.generateApi.hasToken();
+    const model = useGemini ? MODEL_CREDIBILITY : MODEL_PERSONA;
+    onProgress?.({ progress: 55, message: `🤖 ${model} 正在分析线索价值...` });
 
     const prompt = this.buildClassifyPrompt(companyName, emailAddress, domain, emailContent, emailSubject, bgContext);
     const schema = this.buildSchema();
 
     let aiResult: AiClassifyResult;
     try {
-      const raw = await completionJSON(
-        prompt,
-        schema,
-        (_partialContent) => {
+      if (useGemini) {
+        aiResult = await this.generateApi.callAIModelJSON<AiClassifyResult>(
+          model,
+          prompt,
+          {
+            role_content: '你是南驰(Nanchi)公司的外贸线索分析专家。请严格按JSON格式输出,只返回JSON,不要其他文字。',
+            onProgress: (msg) => onProgress?.({ progress: 70, message: msg }),
+          }
+        );
+      } else {
+        const raw = await completionJSON(prompt, schema, (_partialContent) => {
           onProgress?.({ progress: 70, message: '🤖 AI 正在生成分析报告...' });
-        }
-      );
-      aiResult = raw as AiClassifyResult;
+        });
+        aiResult = raw as AiClassifyResult;
+      }
     } catch (err) {
       console.warn('[AiScreen] AI 模型调用失败,降级为规则引擎:', err);
       return this.fallbackClassify(domain, companyName, emailContent, emailSubject, Date.now() - start);
@@ -270,8 +282,8 @@ export class AiScreenService {
 
     const productCatalog = this.buildProductCatalog();
 
-    return `你是犀牛急救(Rhino Rescue)公司的外贸线索分类与价值评估专家。
-犀牛急救主营急救医疗器械(急救包、止血带、夹板、IFAK、壁挂急救箱、EVA户外急救包等),客户覆盖医药分销商、工业安全供应商、政府/NGO采购、品牌OEM、电商零售商。
+    return `你是南驰(Nanchi)公司的外贸线索分类与价值评估专家。
+南驰主营急救医疗器械(急救包、止血带、夹板、IFAK、壁挂急救箱、EVA户外急救包等),客户覆盖医药分销商、工业安全供应商、政府/NGO采购、品牌OEM、电商零售商。
 
 请根据以下信息,判断该线索属于哪种客户画像,并给出**详细**的价值评估。
 即使线索信息有限,也请基于公司名称含义、邮箱域名、行业常识等进行合理推断,给出你的专业判断。
@@ -283,7 +295,7 @@ export class AiScreenService {
 - 邮件主题: ${emailSubject || '无'}
 - 邮件内容: ${(emailContent || '无').substring(0, 1500)}
 ${bgSection}
-## 我方产品目录(犀牛急救真实产品,请从中选择匹配的推荐)
+## 我方产品目录(南驰产品,请从中选择匹配的推荐)
 ${productCatalog}
 
 ## 客户画像定义

+ 112 - 0
lead-discovery/src/app/services/credibility-analysis.service.ts

@@ -0,0 +1,112 @@
+/**
+ * 可信度 / 评分分析服务(使用 gemini-2.5-flash)
+ *
+ * 参考 lead-detail-reuse-mapping.md:
+ * - 可信度/评分:gemini-2.5-flash via GenerateApiService.callAIModel、firecrawlBatchScrape、googleSearch
+ *
+ * 当需要单独刷新可信度、五维评分时调用;完整筛查请使用 AiScreenService。
+ */
+import { Injectable } from '@angular/core';
+import { GenerateApiService } from './generate-api.service';
+
+export interface CredibilityResult {
+  credibilityDetails: { level: 'high' | 'medium' | 'low'; signals: string[] };
+  scores: {
+    demandClarity: number;
+    credibility: number;
+    businessScale: number;
+    productFit: number;
+    urgency: number;
+  };
+  recommendedActions?: {
+    timeframe: string;
+    filesToSend: string[];
+    keyPoints: string[];
+  };
+}
+
+@Injectable({ providedIn: 'root' })
+export class CredibilityAnalysisService {
+
+  private readonly MODEL = 'gemini-2.5-flash';
+
+  constructor(private generateApi: GenerateApiService) {}
+
+  /**
+   * 使用 gemini-2.5-flash 分析可信度与五维评分(需联网数据)
+   */
+  async analyze(
+    companyName: string,
+    domain: string,
+    scrapedContext: string,
+  ): Promise<CredibilityResult | null> {
+    if (!this.generateApi.hasToken() || !scrapedContext?.trim()) {
+      return null;
+    }
+
+    const prompt = this.buildPrompt(companyName, domain, scrapedContext);
+    try {
+      const raw = await this.generateApi.callAIModelJSON<any>(this.MODEL, prompt, {
+        role_content: '你是南驰(Nanchi)公司的线索评估专家。请严格按JSON格式输出,只返回JSON。',
+      });
+      return this.normalize(raw);
+    } catch (err) {
+      console.warn('[CredibilityAnalysis] 分析失败:', err);
+      return null;
+    }
+  }
+
+  private buildPrompt(companyName: string, domain: string, scrapedContext: string): string {
+    return `你是南驰(Nanchi)公司的线索价值评估专家。请根据以下联网抓取数据,评估该公司线索的可信度与五维评分。
+
+## 公司信息
+- 公司名称: ${companyName || '未知'}
+- 域名: ${domain || '未知'}
+
+## 联网抓取数据
+${scrapedContext}
+
+请输出 JSON(只返回 JSON):
+{
+  "credibilityDetails": {
+    "level": "high/medium/low",
+    "signals": ["可信度判断依据1", "依据2"]
+  },
+  "scores": {
+    "demandClarity": 0-100,
+    "credibility": 0-100,
+    "businessScale": 0-100,
+    "productFit": 0-100,
+    "urgency": 0-100
+  },
+  "recommendedActions": {
+    "timeframe": "24h内/3天内/7天内",
+    "filesToSend": ["建议发送的文件"],
+    "keyPoints": ["跟进重点"]
+  }
+}`;
+  }
+
+  private normalize(raw: any): CredibilityResult | null {
+    if (!raw) return null;
+    const level = ['high', 'medium', 'low'].includes(raw.credibilityDetails?.level)
+      ? raw.credibilityDetails.level
+      : 'medium';
+    return {
+      credibilityDetails: {
+        level: level as 'high' | 'medium' | 'low',
+        signals: Array.isArray(raw.credibilityDetails?.signals)
+          ? raw.credibilityDetails.signals
+          : ['企业邮箱', '基础信息完整'],
+      },
+      scores: raw.scores ?? {
+        demandClarity: 60,
+        credibility: 70,
+        businessScale: 70,
+        productFit: 70,
+        urgency: 60,
+      },
+      recommendedActions: raw.recommendedActions,
+    };
+  }
+}

+ 14 - 3
lead-discovery/src/app/services/generate-api.service.ts

@@ -128,24 +128,35 @@ export class GenerateApiService {
   }
 
   /**
-   * 调用 AI 模型(替代 completionJSON,需要 Token)
+   * 调用 AI 模型(支持 gemini-2.5-flash / fmode-1.6-cn 等)
+   * @param model 模型名
+   * @param prompt 用户消息
+   * @param optionsOrProgress role_content 系统角色、onProgress 回调,或直接传 onProgress 函数
    */
   async callAIModelJSON<T = any>(
     model: string,
     prompt: string,
-    onProgress?: (msg: string) => void,
+    optionsOrProgress?: ((msg: string) => void) | { role_content?: string; onProgress?: (msg: string) => void },
   ): Promise<T> {
     this.ensureToken();
     const url = `${this.BASE_URL}/api/apig/generate/minor/${model}`;
+    const opts = typeof optionsOrProgress === 'function'
+      ? { onProgress: optionsOrProgress }
+      : (optionsOrProgress ?? {});
+    const onProgress = opts.onProgress;
+    const roleContent = opts.role_content;
     onProgress?.('正在调用 AI 模型...');
 
+    const body: Record<string, string> = { content: prompt };
+    if (roleContent) body['role_content'] = roleContent;
+
     const response = await fetch(url, {
       method: 'POST',
       headers: {
         'Authorization': `Bearer ${this.getToken()}`,
         'Content-Type': 'application/json',
       },
-      body: JSON.stringify({ content: prompt }),
+      body: JSON.stringify(body),
     });
 
     if (!response.ok) {

+ 141 - 0
lead-discovery/src/app/services/lead-detail-analysis.service.ts

@@ -0,0 +1,141 @@
+/**
+ * lead-detail 板块分析服务
+ *
+ * 参考 lead-detail-reuse-mapping.md:
+ * - 客户画像:EmailAnalysis.customerProfile + requirementExtraction / quickScreenResult
+ * - 快速筛选:Enterprise + ContactInfo / quickScreenResult
+ * - 需求摘要:requirementExtraction._rawDemands / quickScreenResult.demandSnapshot
+ * - 原始邮件:Parse Email 表
+ * - 推荐产品:quickScreenResult.suggestedProducts
+ * - 可信度/评分/AI下一步:quickScreenResult.scores, credibilityDetails, recommendedActions
+ */
+import { Injectable } from '@angular/core';
+import { ParseDataService } from './parse-data.service';
+import { AiScreenService } from './ai-screen.service';
+import { GenerateApiService } from './generate-api.service';
+import { Email, Lead, QuickScreenResult } from '../models/lead.model';
+
+export interface LeadAnalysisData {
+  email: Email | null;
+  /** 是否从 Parse 加载的邮件 */
+  emailFromParse: boolean;
+  /** 是否已运行 AI 补充分析 */
+  enrichedByAI: boolean;
+}
+
+@Injectable({ providedIn: 'root' })
+export class LeadDetailAnalysisService {
+
+  constructor(
+    private parseData: ParseDataService,
+    private aiScreen: AiScreenService,
+    private generateApi: GenerateApiService,
+  ) {}
+
+  /**
+   * 为 lead 加载完整分析数据(邮件从 Parse 补充)
+   */
+  async loadAnalysisForLead(
+    lead: Lead,
+    existingEmails: Email[],
+  ): Promise<LeadAnalysisData> {
+    const result: LeadAnalysisData = {
+      email: null,
+      emailFromParse: false,
+      enrichedByAI: false,
+    };
+
+    // 1. 邮件:优先内存,否则从 Parse 加载
+    const fromMemory = existingEmails.find(e => e.leadId === lead.id)
+      ?? existingEmails.find(e => e.from === lead.contactEmail);
+    if (fromMemory) {
+      result.email = fromMemory;
+      return result;
+    }
+
+    if (!this.parseData.enabled) return result;
+
+    let parsed: Email | null = null;
+    if (lead.id) {
+      parsed = await this.parseData.getEmailByLeadId(lead.id);
+    }
+    if (!parsed && lead.contactEmail) {
+      parsed = await this.parseData.getEmailByCustomerEmail(lead.contactEmail);
+    }
+    if (parsed) {
+      result.email = parsed;
+      result.emailFromParse = true;
+    }
+    return result;
+  }
+
+  /**
+   * 当 quickScreenResult 缺少 scores/demandSnapshot/credibilityDetails/recommendedActions 时,
+   * 调用 AI 补充分析(需要 Token)
+   */
+  async enrichAnalysis(lead: Lead, onProgress?: (msg: string) => void): Promise<QuickScreenResult | null> {
+    const qsr = lead.quickScreenResult;
+    const hasScores = qsr?.scores != null;
+    const hasDemand =
+      (lead.extractedProductDemands?.length ?? 0) > 0 ||
+      (qsr?.demandSnapshot?.products?.length ?? 0) > 0;
+    const hasCredibility = qsr?.credibilityDetails != null;
+    const hasRecommended = (qsr?.recommendedActions?.keyPoints?.length ?? 0) > 0;
+
+    if (hasScores && hasDemand && hasCredibility) {
+      return null;
+    }
+
+    if (!this.generateApi.hasToken()) {
+      return null;
+    }
+
+    try {
+      onProgress?.('🔍 正在调用 AI 补充分析...');
+      const domain = this.extractDomain(lead.contactEmail, lead.quickScreenResult?.domain);
+      const result = await this.aiScreen.screenWithAI(
+        lead.companyName,
+        lead.contactEmail,
+        '', // 无邮件正文时仅用公司+邮箱+联网
+        '',
+        (p) => onProgress?.(p.message),
+      );
+      onProgress?.('✅ 分析完成');
+      return result;
+    } catch (err) {
+      console.warn('[LeadDetailAnalysis] AI 补充分析失败:', err);
+      return null;
+    }
+  }
+
+  /**
+   * 合并 AI 补充结果到 quickScreenResult(不覆盖已有字段)
+   */
+  mergeEnrichedResult(
+    existing: QuickScreenResult,
+    enriched: QuickScreenResult,
+  ): QuickScreenResult {
+    return {
+      ...existing,
+      scores: existing.scores ?? enriched.scores,
+      demandSnapshot:
+        (existing.demandSnapshot?.products?.length ?? 0) > 0
+          ? existing.demandSnapshot
+          : enriched.demandSnapshot,
+      credibilityDetails: existing.credibilityDetails ?? enriched.credibilityDetails,
+      recommendedActions: (existing.recommendedActions?.keyPoints?.length ?? 0) > 0
+        ? existing.recommendedActions
+        : enriched.recommendedActions,
+      matchedKeywords: (existing.matchedKeywords?.length ?? 0) > 0
+        ? existing.matchedKeywords
+        : enriched.matchedKeywords,
+    };
+  }
+
+  private extractDomain(email: string, fallback?: string): string {
+    if (email?.includes('@')) {
+      return email.split('@')[1]?.toLowerCase() || '';
+    }
+    return fallback || '';
+  }
+}

+ 29 - 6
lead-discovery/src/app/services/mock-data.service.ts

@@ -37,7 +37,7 @@ export class MockDataService {
   readonly emails: Email[] = [
     {
       id: 'e1', from: 'john.smith@acmemedical.com', fromName: 'John Smith',
-      to: 'sales@rhinorescue.com', cc: 'purchasing@acmemedical.com',
+      to: 'sales@nanchi.com', cc: 'purchasing@acmemedical.com',
       subject: 'Inquiry: Bulk IFAK Kits for Hospital Network',
       preview: 'Dear Sir/Madam, We are a medical supply distributor based in Ohio, USA. We are looking for a reliable manufacturer of IFAK kits for our hospital network clients...',
       body: `Dear Sir/Madam,
@@ -79,7 +79,7 @@ Email: john.smith@acmemedical.com`,
     },
     {
       id: 'e2', from: 'procurement@safetyfirst-uk.co.uk', fromName: 'Sarah Johnson',
-      to: 'sales@rhinorescue.com',
+      to: 'sales@nanchi.com',
       subject: 'RE: OSHA Compliant First Aid Cabinets - Quote Request',
       preview: 'Hi, Following our conversation at the Birmingham Safety Expo, we would like to request a formal quotation for wall-mounted first aid cabinets...',
       body: `Hi,
@@ -129,7 +129,7 @@ Email: procurement@safetyfirst-uk.co.uk`,
     },
     {
       id: 'e3', from: 'tender@redcross-pl.org', fromName: 'Marek Kowalski',
-      to: 'sales@rhinorescue.com', cc: 'logistics@redcross-pl.org, legal@redcross-pl.org',
+      to: 'sales@nanchi.com', cc: 'logistics@redcross-pl.org, legal@redcross-pl.org',
       subject: 'Tender Notice: Emergency Medical Backpacks - Ref#2026/RC/045',
       preview: 'Dear Supplier, Polish Red Cross is inviting tenders for the supply of 500 emergency medical backpacks for disaster relief operations...',
       body: `Dear Supplier,
@@ -188,7 +188,7 @@ Email: tender@redcross-pl.org`,
     },
     {
       id: 'e4', from: 'alex@wildgear.de', fromName: 'Alexander Müller',
-      to: 'sales@rhinorescue.com',
+      to: 'sales@nanchi.com',
       subject: 'Custom First Aid Kits for Outdoor Brand - OEM Inquiry',
       preview: 'Hello, We are WildGear, a German outdoor sports brand. We are looking for a manufacturer to produce private label first aid kits with our branding...',
       body: `Hello,
@@ -241,7 +241,7 @@ Web: www.wildgear.de`,
     },
     {
       id: 'e5', from: 'info@globaltrading88.com', fromName: 'Mike Chen',
-      to: 'sales@rhinorescue.com',
+      to: 'sales@nanchi.com',
       subject: 'Wholesale Various Products',
       preview: 'Hello friend, we are looking for all kinds of products for wholesale. Can you send us your full catalog with best prices? We buy everything...',
       body: `Hello friend,
@@ -266,7 +266,7 @@ info@globaltrading88.com`,
     },
     {
       id: 'e6', from: 'lisa@meditechsupply.ca', fromName: 'Lisa Wang',
-      to: 'sales@rhinorescue.com', cc: 'bd@meditechsupply.ca',
+      to: 'sales@nanchi.com', cc: 'bd@meditechsupply.ca',
       subject: 'Partnership Inquiry - Canadian Medical Distributor',
       preview: 'Dear Rhino Rescue Team, MediTech Supply is a leading medical equipment distributor in Canada with 15 years of experience. We are interested in becoming your authorized distributor...',
       body: `Dear Rhino Rescue Team,
@@ -338,6 +338,29 @@ Web: www.meditechsupply.ca`,
     this.createLead('l12', 'SafetyFirst', 'sales@safetyfirst-uk.co.uk', 'David Brown', 'GB', 'B', 'B', 'P2', 'platform', 'screened', 25000, new Date('2026-02-15')),
   ];
 
+  /** 运行时导入的邮件(EML 导入后添加),供 lead-detail 等展示 */
+  private dynamicEmails: Email[] = [];
+  /** 运行时创建的线索(快速筛选后添加),供 list、lead-detail 使用 */
+  private dynamicLeads: Lead[] = [];
+
+  /** 全部邮件 = 模拟数据 + 导入的 */
+  get allEmails(): Email[] {
+    return [...this.emails, ...this.dynamicEmails];
+  }
+
+  /** 全部线索 = 模拟数据 + 新建的 */
+  get allLeads(): Lead[] {
+    return [...this.leads, ...this.dynamicLeads];
+  }
+
+  addEmail(email: Email): void {
+    this.dynamicEmails.push(email);
+  }
+
+  addLead(lead: Lead): void {
+    this.dynamicLeads.push(lead);
+  }
+
   // ===== AI 挖掘批次(模拟) =====
   readonly discoveryBatches: AiDiscoveryBatch[] = [
     {

+ 233 - 0
lead-discovery/src/app/services/parse-data.service.ts

@@ -0,0 +1,233 @@
+/**
+ * Parse 数据服务:保存 Email、Lead 到 fmode/Parse 后端
+ * 参考 ltc-nanchi email-import-workflow、parse-data
+ */
+import { Injectable } from '@angular/core';
+import { FmodeParse } from 'fmode-ng/core';
+import { Email, Lead } from '../models/lead.model';
+
+const Parse = FmodeParse.with('nova');
+
+@Injectable({ providedIn: 'root' })
+export class ParseDataService {
+
+  /** 是否启用 Parse 持久化(未登录或网络异常时可设为 false 仅用内存) */
+  get enabled(): boolean {
+    try {
+      return typeof Parse !== 'undefined';
+    } catch {
+      return false;
+    }
+  }
+
+  /**
+   * 保存 Email 到 Parse
+   * @returns Parse objectId,失败时返回 null
+   */
+  async saveEmail(email: Email): Promise<string | null> {
+    try {
+      const obj = new Parse.Object('Email');
+      obj.set('messageId', email.id);
+      obj.set('fromEmail', email.from);
+      obj.set('fromName', email.fromName);
+      obj.set('toEmails', email.to ? email.to.split(',').map(s => s.trim()) : []);
+      if (email.cc) obj.set('ccEmails', email.cc.split(',').map(s => s.trim()));
+      obj.set('subject', email.subject);
+      obj.set('content', email.body);
+      obj.set('preview', email.preview);
+      if (email.htmlContent) obj.set('htmlContent', email.htmlContent);
+      obj.set('receivedAt', email.receivedAt);
+      obj.set('customerEmail', email.from);
+      obj.set('customerName', email.fromName);
+      obj.set('status', 'unread');
+      obj.set('source', 'imported_eml');
+      obj.set('attachments', email.attachments.map(a => ({
+        id: a.id,
+        filename: a.fileName,
+        mimeType: a.mimeType,
+        size: a.fileSize,
+        url: a.url && !a.url.startsWith('blob:') ? a.url : undefined,
+      })));
+      obj.set('hasScreened', email.hasScreened);
+      if (email.leadId) obj.set('leadId', email.leadId);
+      if (email.screenResult) obj.set('screenResult', JSON.stringify(email.screenResult));
+
+      this.setUserIfLoggedIn(obj);
+      const saved = await obj.save();
+      const id = saved.id as string;
+      console.log('[ParseData] Email saved:', id);
+      return id;
+    } catch (err) {
+      console.warn('[ParseData] saveEmail failed:', err);
+      return null;
+    }
+  }
+
+  /**
+   * 更新 Email 的 leadId 关联
+   */
+  async updateEmailLeadId(emailParseId: string, leadParseId: string): Promise<boolean> {
+    try {
+      const query = new Parse.Query('Email');
+      const obj = await query.get(emailParseId);
+      if (!obj) return false;
+      obj.set('leadId', leadParseId);
+      obj.set('hasScreened', true);
+      this.setUserIfLoggedIn(obj);
+      await obj.save();
+      console.log('[ParseData] Email leadId updated:', emailParseId, '->', leadParseId);
+      return true;
+    } catch (err) {
+      console.warn('[ParseData] updateEmailLeadId failed:', err);
+      return false;
+    }
+  }
+
+  /**
+   * 保存 Lead 到 Parse
+   * @returns Parse objectId,失败时返回 null
+   */
+  async saveLead(lead: Lead): Promise<string | null> {
+    try {
+      const obj = new Parse.Object('Lead');
+      obj.set('leadNumber', lead.leadNumber);
+      obj.set('contactName', lead.contactName);
+      obj.set('contactEmail', lead.contactEmail);
+      obj.set('companyName', lead.companyName);
+      obj.set('country', lead.country);
+      obj.set('source', lead.source);
+      obj.set('sourceLabel', lead.sourceLabel);
+      obj.set('customerCategory', lead.customerCategory);
+      obj.set('categoryLabel', lead.categoryLabel);
+      obj.set('valueGrade', lead.valueGrade);
+      obj.set('persona', lead.persona);
+      obj.set('personaLabel', lead.personaLabel);
+      obj.set('personaConfidence', lead.personaConfidence);
+      obj.set('personaVerified', lead.personaVerified);
+      obj.set('quickScreenResult', lead.quickScreenResult as unknown as Record<string, unknown>);
+      obj.set('quickScreenAt', lead.quickScreenAt);
+      obj.set('hasDeepAnalysis', lead.hasDeepAnalysis);
+      obj.set('followUpStage', lead.followUpStage);
+      obj.set('nextFollowUpDate', lead.nextFollowUpDate);
+      obj.set('followUpNotes', lead.followUpNotes || '');
+      obj.set('matchedProductSKUs', lead.matchedProductSKUs || []);
+      obj.set('estimatedOrderValue', lead.estimatedOrderValue || 0);
+      obj.set('hasBackgroundCheck', lead.hasBackgroundCheck);
+      obj.set('entityId', lead.entityId);
+      obj.set('domainKey', lead.domainKey);
+      obj.set('emailKey', lead.emailKey);
+      obj.set('relatedLeadIds', lead.relatedLeadIds || []);
+      obj.set('isEntityPrimary', lead.isEntityPrimary);
+      obj.set('createdAt', lead.createdAt);
+      if (lead.backgroundCheckReport) {
+        obj.set('backgroundCheckReport', lead.backgroundCheckReport as unknown as Record<string, unknown>);
+      }
+      if (lead.aiResearchReport) {
+        obj.set('aiResearchReport', lead.aiResearchReport as unknown as Record<string, unknown>);
+      }
+      if (lead.salesFeedback) {
+        obj.set('salesFeedback', lead.salesFeedback as unknown as Record<string, unknown>);
+      }
+
+      this.setUserIfLoggedIn(obj);
+      const saved = await obj.save();
+      const id = saved.id as string;
+      console.log('[ParseData] Lead saved:', id);
+      return id;
+    } catch (err) {
+      console.warn('[ParseData] saveLead failed:', err);
+      return null;
+    }
+  }
+
+  /**
+   * 按 leadId 查询关联的 Email(用于 lead-detail 原始邮件展示)
+   */
+  async getEmailByLeadId(leadId: string): Promise<Email | null> {
+    try {
+      const query = new Parse.Query('Email');
+      query.equalTo('leadId', leadId);
+      const obj = await query.first();
+      return obj ? this.parseObjToEmail(obj) : null;
+    } catch (err) {
+      console.warn('[ParseData] getEmailByLeadId failed:', err);
+      return null;
+    }
+  }
+
+  /**
+   * 按 customerEmail 查询 Email(用于无 leadId 时按发件人匹配)
+   */
+  async getEmailByCustomerEmail(customerEmail: string): Promise<Email | null> {
+    if (!customerEmail?.includes('@')) return null;
+    try {
+      const query = new Parse.Query('Email');
+      query.equalTo('customerEmail', customerEmail);
+      query.descending('receivedAt');
+      const obj = await query.first();
+      return obj ? this.parseObjToEmail(obj) : null;
+    } catch (err) {
+      console.warn('[ParseData] getEmailByCustomerEmail failed:', err);
+      return null;
+    }
+  }
+
+  private parseObjToEmail(obj: any): Email {
+    const toArr = obj.get('toEmails');
+    const to = Array.isArray(toArr) ? toArr.join(', ') : (obj.get('toEmails') || '');
+    const ccArr = obj.get('ccEmails');
+    const cc = Array.isArray(ccArr) ? ccArr.join(', ') : undefined;
+    const attRaw = obj.get('attachments') || [];
+    const attachments = (Array.isArray(attRaw) ? attRaw : []).map((a: any, i: number) => ({
+      id: a.id || `att-${i}-${Date.now()}`,
+      fileName: a.filename || a.fileName || 'attachment',
+      fileSize: a.size || a.fileSize || 0,
+      mimeType: a.mimeType || 'application/octet-stream',
+      icon: this.mimeToIcon(a.mimeType),
+      url: a.url,
+    }));
+    const screenResultRaw = obj.get('screenResult');
+    let screenResult: any;
+    if (typeof screenResultRaw === 'string') {
+      try {
+        screenResult = JSON.parse(screenResultRaw);
+      } catch {}
+    } else if (screenResultRaw && typeof screenResultRaw === 'object') {
+      screenResult = screenResultRaw;
+    }
+    return {
+      id: obj.get('messageId') || obj.id,
+      from: obj.get('fromEmail') || obj.get('customerEmail') || '',
+      fromName: obj.get('fromName') || obj.get('customerName') || '',
+      to,
+      cc,
+      subject: obj.get('subject') || '',
+      preview: obj.get('preview') || '',
+      body: obj.get('content') || obj.get('body') || '',
+      htmlContent: obj.get('htmlContent') || undefined,
+      attachments,
+      receivedAt: obj.get('receivedAt') ? new Date(obj.get('receivedAt')) : new Date(),
+      hasScreened: obj.get('hasScreened') ?? false,
+      screenResult,
+      leadId: obj.get('leadId') || undefined,
+    };
+  }
+
+  private mimeToIcon(mime: string): string {
+    if (!mime) return 'insert_drive_file';
+    if (mime.includes('pdf')) return 'picture_as_pdf';
+    if (/sheet|excel|xlsx?|csv/i.test(mime)) return 'table_chart';
+    if (/image|png|jpeg|gif/i.test(mime)) return 'image';
+    return 'insert_drive_file';
+  }
+
+  private setUserIfLoggedIn(parseObject: any): void {
+    try {
+      const user = Parse.User.current();
+      if (user) {
+        parseObject.set('user', user);
+        parseObject.set('assignedTo', user);
+      }
+    } catch {}
+  }
+}