Skill: cjboy007/ssa-campaign-tracker (version 1.0.0)

--- name: campaign-tracker description: 客户开发信追踪与分析 — 自动归档发送记录、匹配客户回复、生成效果分析报告、优化邮件模板 --- # campaign-tracker SKILL > 客户开发信追踪与分析 — 完整闭环系统 ## 用途 当需要以下操作时使用此 skill： - 归档开发信发送记录 - 追踪客户回复状态 - 生成开发信效果分析报告（周报/月报） - 优化开发信模板（A/B测试建议） ## 前置条件 1. task-001（`imap-smtp-email`）已配置并能正常发送邮件 2. task-002（`okki-email-sync`）已配置，OKKI 向量搜索可用 3. Node.js 环境可用 ## 快速开始 ```bash # 切换到 skill 目录 cd <path-to-campaign-tracker> # 或使用环境变量 cd $CAMPAIGN_TRACKER_ROOT ``` ## 核心命令 ### 归档已发送开发信 ```bash node scripts/archive-sent-records.js check --dry-run # 预览 node scripts/archive-sent-records.js check # 执行归档 ``` ### 匹配客户回复 ```bash node scripts/reply-matcher.js check --dry-run # 预览 node scripts/reply-matcher.js check # 执行匹配 node scripts/reply-matcher.js unmatched # 查看未匹配 ``` ### 生成分析报告 ```bash # 周报 node scripts/analytics-report.js weekly node scripts/analytics-report.js weekly --obsidian # 同时输出到 Obsidian # 月报 node scripts/analytics-report.js monthly node scripts/analytics-report.js monthly --last-month # 上个月 ``` ### 模板优化建议 ```bash node scripts/template-optimizer.js analyze --dry-run # 预览 node scripts/template-optimizer.js analyze # 执行分析 node scripts/template-optimizer.js analyze --obsidian # 输出到 Obsidian ``` ## 文件说明 | 文件 | 用途 | |------|------| | `config/tracking-schema.json` | 数据模型配置（5模块） | | `scripts/archive-sent-records.js` | 发送记录归档（512行） | | `scripts/reply-matcher.js` | 回复匹配+状态更新（669行） | | `scripts/analytics-report.js` | 效果分析报告（542行） | | `scripts/template-optimizer.js` | 模板优化+A/B测试（625行） | ## 典型工作流 1. 每次发送开发信后 → 运行 `archive-sent-records.js check` 2. 每天检查新回复 → 运行 `reply-matcher.js check` 3. 每周一 → 运行 `analytics-report.js weekly --obsidian` 4. 每月初 → 运行 `analytics-report.js monthly --obsidian` 5. 每月优化模板 → 运行 `template-optimizer.js analyze --obsidian` ## 数据位置 ``` <campaign-tracker-root>/ ├── archive/ # 发送记录归档（JSONL） ├── reply-tracking/ # 回复状态数据（JSONL） ├── reports/ # 分析报告（Markdown+JSON） └── logs/ # 运行日志 $OBSIDIAN_VAULT/开发信追踪/ # Obsidian 知识库输出 ``` ## 依赖集成 ```javascript // archive-sent-records.js 读取 smtp.js 发送日志 const emailSkillPath = process.env.EMAIL_SKILL_ROOT || '<path-to-imap-smtp-email>'; // reply-matcher.js 使用 IMAP 收件箱 const imapConfig = require(process.env.EMAIL_SKILL_ROOT + '/.env'); // 客户匹配使用 OKKI 向量搜索 const vectorSearch = process.env.OKKI_VECTOR_SEARCH || '<path-to-okki_vector_search_v3.py>'; ``` ## 注意事项 - 首次运行前确保 `archive/`、`reply-tracking/`、`reports/`、`logs/` 目录已存在（脚本会自动创建） - 所有脚本支持 `--dry-run` 模式，建议首次运行时使用 - Obsidian 输出需要确认 vault 路径：`~/obsidian-vault/` - A/B 测试配置存储在 `config/tracking-schema.json` 的 `ab_testing` 模块

path: README.md
content: # campaign-tracker

> 客户开发信追踪与分析系统 — 完整闭环：发送 → 追踪 → 分析 → 优化

## 概述

campaign-tracker 是 Farreach 开发信营销闭环系统，集成邮件发送记录归档、回复状态追踪、效果分析报告和模板优化建议四大功能模块。

## 依赖

- **task-001** (`imap-smtp-email`)：邮件发送系统，提供已发送邮件数据
- **task-002** (`okki-email-sync`)：OKKI CRM 同步，客户匹配数据

## 目录结构

```
campaign-tracker/
├── config/
│   └── tracking-schema.json      # 数据模型配置（5个核心模块）
├── scripts/
│   ├── archive-sent-records.js   # 发送记录自动归档
│   ├── reply-matcher.js          # 回复匹配与状态更新
│   ├── analytics-report.js       # 效果分析报告（周报/月报）
│   └── template-optimizer.js     # 模板优化建议（A/B测试）
├── archive/                      # 归档数据目录（JSONL格式）
├── reply-tracking/               # 回复追踪数据
├── reports/                      # 分析报告输出
├── logs/                         # 运行日志
├── README.md
└── SKILL.md
```

## 核心模块

### 1. 数据模型 (`tracking-schema.json`)
5个核心数据模块：
- `sent_records`：12字段发送记录（recipient、template、campaign_id、subject等）
- `reply_tracking`：11字段回复追踪（含AI意图分类）
- `conversion_funnel`：6阶段转化漏斗
- `metrics`：多维度指标（campaign/template/sales_owner）
- `ab_testing`：A/B测试配置

### 2. 发送记录归档 (`archive-sent-records.js`)
```bash
# 检查已发送邮件并归档
node scripts/archive-sent-records.js check

# 干跑模式（预览，不写入）
node scripts/archive-sent-records.js check --dry-run

# 手动归档指定邮件
node scripts/archive-sent-records.js archive --uid <UID>
```

### 3. 回复匹配 (`reply-matcher.js`)
```bash
# 检查收件箱并匹配回复
node scripts/reply-matcher.js check

# 干跑模式
node scripts/reply-matcher.js check --dry-run

# 查看未匹配回复
node scripts/reply-matcher.js unmatched
```

**匹配策略：**
- 邮件地址精确匹配
- Subject 关键词模糊匹配
- 时间窗口过滤（30天内）
- AI意图分类（interested/not_interested/need_info/pricing/sample_request/no_reply）

### 4. 效果分析报告 (`analytics-report.js`)
```bash
# 生成本周周报
node scripts/analytics-report.js weekly

# 生成上月月报
node scripts/analytics-report.js monthly --last-month

# 输出到 Obsidian 知识库
node scripts/analytics-report.js weekly --obsidian
```

**报告内容：**
- 发送总量、回复率、转化率
- 转化漏斗分析
- 模板对比分析
- 智能洞察建议

### 5. 模板优化 (`template-optimizer.js`)
```bash
# 分析并生成优化建议
node scripts/template-optimizer.js analyze

# 干跑模式
node scripts/template-optimizer.js analyze --dry-run

# 输出到 Obsidian
node scripts/template-optimizer.js analyze --obsidian
```

**优化逻辑：**
- 识别高效模板（回复率 Top 30%）
- 识别低效模板（回复率 Bottom 30%）
- 生成改进建议
- 推荐 A/B 测试计划

## 工作流程

```
发送开发信（task-001）
       ↓
archive-sent-records.js → archive/ JSONL 归档
       ↓
reply-matcher.js → 匹配回复，更新状态
       ↓
analytics-report.js → 生成周报/月报
       ↓
template-optimizer.js → 优化建议 + A/B测试计划
       ↓
Obsidian 知识库归档
```

## 数据存储

| 数据类型 | 位置 | 格式 |
|---------|------|------|
| 发送记录 | `archive/sent-{campaign_id}.jsonl` | JSONL |
| 回复追踪 | `reply-tracking/replies-{date}.jsonl` | JSONL |
| 分析报告 | `reports/` | Markdown + JSON |
| 优化建议 | `reports/optimization-{date}.md` | Markdown |
| Obsidian | `~/obsidian-vault/开发信追踪/` | Markdown |

## 集成说明

### 与 task-001（imap-smtp-email）集成
archive-sent-records.js 读取 smtp.js 发送后的日志，自动提取发送记录字段。

### 与 task-002（okki-email-sync）集成
客户匹配使用 okki-email-sync 的向量检索能力，将发送记录关联到 OKKI 客户ID。

### Obsidian 输出
报告自动输出到 `~/obsidian-vault/开发信追踪/` 目录，与知识库无缝集成。

## 开发信息

- **版本：** 1.0.0
- **状态：** 生产就绪
- **构建时间：** 2026-03-24
- **task_id：** task-004


path: config/tracking-schema.json
content: {
  "sent_records": {
    "description": "开发信发送记录字段定义",
    "fields": {
      "record_id": {
        "type": "string",
        "format": "uuid",
        "description": "唯一记录 ID"
      },
      "campaign_id": {
        "type": "string",
        "description": "关联的营销活动 ID"
      },
      "recipient_email": {
        "type": "string",
        "format": "email",
        "description": "收件人邮箱"
      },
      "recipient_name": {
        "type": "string",
        "description": "收件人姓名"
      },
      "company_name": {
        "type": "string",
        "description": "收件人公司名"
      },
      "template_id": {
        "type": "string",
        "description": "使用的邮件模板 ID"
      },
      "template_variant": {
        "type": "string",
        "enum": [
          "A",
          "B",
          "C"
        ],
        "description": "A/B 测试变体标识"
      },
      "subject_line": {
        "type": "string",
        "description": "邮件主题"
      },
      "sent_timestamp": {
        "type": "string",
        "format": "date-time",
        "description": "发送时间戳"
      },
      "sales_owner": {
        "type": "string",
        "description": "负责的销售人员"
      },
      "send_status": {
        "type": "string",
        "enum": [
          "sent",
          "failed",
          "bounced",
          "deferred"
        ],
        "description": "发送状态"
      },
      "error_message": {
        "type": "string",
        "description": "发送失败时的错误信息"
      }
    },
    "required_fields": [
      "record_id",
      "campaign_id",
      "recipient_email",
      "template_id",
      "sent_timestamp",
      "sales_owner",
      "send_status"
    ]
  },
  "reply_tracking": {
    "description": "回复追踪字段定义",
    "fields": {
      "reply_id": {
        "type": "string",
        "format": "uuid",
        "description": "回复记录唯一 ID"
      },
      "sent_record_id": {
        "type": "string",
        "format": "uuid",
        "description": "关联的发送记录 ID"
      },
      "reply_received": {
        "type": "boolean",
        "description": "是否收到回复"
      },
      "reply_timestamp": {
        "type": "string",
        "format": "date-time",
        "description": "回复收到时间戳"
      },
      "reply_type": {
        "type": "string",
        "enum": [
          "positive",
          "negative",
          "neutral",
          "inquiry",
          "out_of_office"
        ],
        "description": "回复类型分类"
      },
      "auto_classified_by_intent": {
        "type": "boolean",
        "description": "是否由 AI 自动分类意图"
      },
      "detected_intent": {
        "type": "string",
        "description": "检测到的意图（对接 task-001 intent 系统）"
      },
      "confidence_score": {
        "type": "number",
        "minimum": 0,
        "maximum": 1,
        "description": "意图分类置信度"
      },
      "reply_snippet": {
        "type": "string",
        "description": "回复内容摘要（前 200 字符）"
      },
      "requires_manual_review": {
        "type": "boolean",
        "description": "是否需要人工审核"
      },
      "follow_up_action": {
        "type": "string",
        "enum": [
          "quote",
          "sample",
          "call",
          "meeting",
          "close",
          "discard"
        ],
        "description": "建议的后续动作"
      }
    },
    "required_fields": [
      "reply_id",
      "sent_record_id",
      "reply_received"
    ]
  },
  "conversion_funnel": {
    "description": "转化漏斗阶段定义",
    "stages": [
      {
        "stage_id": "sent",
        "stage_name": "已发送",
        "description": "邮件已成功发送",
        "entry_condition": "send_status = 'sent'",
        "exit_condition": "reply_received = true OR bounce_detected = true",
        "metrics": [
          "count",
          "rate"
        ]
      },
      {
        "stage_id": "opened",
        "stage_name": "已打开",
        "description": "收件人打开了邮件（需像素追踪支持）",
        "entry_condition": "open_event_detected = true",
        "exit_condition": "reply_received = true OR 7_days_no_action",
        "metrics": [
          "count",
          "rate",
          "open_time_avg"
        ]
      },
      {
        "stage_id": "replied",
        "stage_name": "已回复",
        "description": "收件人回复了邮件",
        "entry_condition": "reply_received = true",
        "exit_condition": "qualified = true OR discarded = true",
        "metrics": [
          "count",
          "rate",
          "response_time_avg"
        ]
      },
      {
        "stage_id": "qualified",
        "stage_name": "合格线索",
        "description": "回复被判定为有效询盘",
        "entry_condition": "reply_type IN ('positive', 'inquiry') AND manual_review_passed = true",
        "exit_condition": "converted = true OR lost = true",
        "metrics": [
          "count",
          "rate",
          "qualification_time_avg"
        ]
      },
      {
        "stage_id": "converted",
        "stage_name": "已转化",
        "description": "线索转化为样品单或订单",
        "entry_condition": "sample_order_created = true OR sales_order_created = true",
        "exit_condition": "closed_won = true OR closed_lost = true",
        "metrics": [
          "count",
          "rate",
          "conversion_time_avg"
        ]
      },
      {
        "stage_id": "closed_won",
        "stage_name": "已成交",
        "description": "成功获得订单",
        "entry_condition": "order_confirmed = true AND deposit_received = true",
        "exit_condition": "none",
        "metrics": [
          "count",
          "rate",
          "revenue"
        ]
      }
    ],
    "drop_off_analysis": {
      "enabled": true,
      "alert_threshold": 0.5,
      "description": "当阶段间流失率超过 50% 时告警"
    }
  },
  "metrics": {
    "description": "统计指标定义",
    "campaign_level": {
      "reply_rate": {
        "formula": "replied_count / sent_count",
        "description": "回复率",
        "target": 0.15,
        "alert_if_below": 0.05
      },
      "conversion_rate": {
        "formula": "converted_count / sent_count",
        "description": "转化率",
        "target": 0.03,
        "alert_if_below": 0.01
      },
      "avg_response_time": {
        "formula": "SUM(reply_timestamp - sent_timestamp) / replied_count",
        "description": "平均响应时间（小时）",
        "unit": "hours"
      },
      "open_rate": {
        "formula": "opened_count / sent_count",
        "description": "打开率（需像素追踪）",
        "target": 0.4,
        "alert_if_below": 0.2
      },
      "bounce_rate": {
        "formula": "bounced_count / sent_count",
        "description": "退信率",
        "alert_if_above": 0.05
      }
    },
    "template_performance": {
      "metrics": [
        "reply_rate",
        "conversion_rate",
        "open_rate"
      ],
      "comparison_basis": "template_id",
      "min_sample_size": 50,
      "description": "按模板 ID 对比性能指标"
    },
    "sales_owner_performance": {
      "metrics": [
        "reply_rate",
        "conversion_rate",
        "avg_response_time"
      ],
      "comparison_basis": "sales_owner",
      "description": "按销售人员对比性能指标"
    },
    "time_series": {
      "granularity": [
        "daily",
        "weekly",
        "monthly"
      ],
      "metrics": [
        "sent_count",
        "replied_count",
        "converted_count"
      ],
      "description": "时间序列趋势分析"
    }
  },
  "ab_testing": {
    "description": "A/B 测试配置字段",
    "test_config": {
      "variant_a": {
        "template_id": "string",
        "subject_pattern": "string",
        "send_time": "string",
        "description": "对照组配置"
      },
      "variant_b": {
        "template_id": "string",
        "subject_pattern": "string",
        "send_time": "string",
        "description": "实验组配置"
      },
      "sample_size": {
        "type": "integer",
        "description": "每组样本数量",
        "min": 100,
        "recommended": 500
      },
      "split_ratio": {
        "type": "object",
        "properties": {
          "variant_a": {
            "type": "number",
            "default": 0.5
          },
          "variant_b": {
            "type": "number",
            "default": 0.5
          }
        },
        "description": "流量分配比例"
      },
      "success_metric": {
        "type": "string",
        "enum": [
          "reply_rate",
          "conversion_rate",
          "open_rate"
        ],
        "default": "reply_rate",
        "description": "成功判定指标"
      },
      "statistical_significance": {
        "type": "number",
        "default": 0.95,
        "description": "统计显著性水平（置信度）"
      },
      "min_detectable_effect": {
        "type": "number",
        "default": 0.1,
        "description": "最小可检测效应（10% 提升）"
      }
    },
    "test_results": {
      "fields": {
        "test_id": {
          "type": "string",
          "format": "uuid"
        },
        "start_date": {
          "type": "string",
          "format": "date"
        },
        "end_date": {
          "type": "string",
          "format": "date"
        },
        "variant_a_metrics": {
          "type": "object"
        },
        "variant_b_metrics": {
          "type": "object"
        },
        "winner": {
          "type": "string",
          "enum": [
            "A",
            "B",
            "inconclusive"
          ]
        },
        "p_value": {
          "type": "number"
        },
        "confidence_interval": {
          "type": "object"
        },
        "recommendation": {
          "type": "string"
        }
      }
    },
    "test_groups": [
      {
        "test_id": "test-2026-03-24-01",
        "name": "高效模板特征验证：template-intro-v1",
        "hypothesis": "template-intro-v1 的高回复率源于其low_personalization特征",
        "variant_a": {
          "description": "对照组：当前低效模板",
          "template_id": "template-intro-v2"
        },
        "variant_b": {
          "description": "实验组：高效模板特征",
          "template_id": "template-intro-v1"
        },
        "success_metric": "reply_rate",
        "sample_size": 100,
        "split_ratio": {
          "variant_a": 0.5,
          "variant_b": 0.5
        },
        "statistical_significance": 0.95,
        "status": "recommended",
        "priority": "high",
        "created_at": "2026-03-24T07:37:45.488Z"
      }
    ]
  },
  "data_retention": {
    "sent_records": "365_days",
    "reply_tracking": "365_days",
    "aggregated_metrics": "indefinite",
    "ab_test_results": "indefinite",
    "description": "数据保留策略"
  },
  "integrations": {
    "task_001_email_system": {
      "description": "对接 email-smart-reply 模板系统",
      "template_sync": true,
      "send_trigger": true
    },
    "task_002_okki_sync": {
      "description": "对接 okki-email-sync 跟进记录",
      "contact_sync": true,
      "reply_classification_sync": true
    },
    "obsidian_knowledge_base": {
      "description": "分析报告存入 Obsidian 知识库",
      "weekly_report": true,
      "monthly_report": true
    }
  }
}

path: reports/optimizer-report-2026-03-24.json
content: {
  "generated_at": "2026-03-24T07:37:45.487Z",
  "total_templates": 2,
  "thresholds": {
    "top30": 0.5,
    "bottom30": 0
  },
  "high_performers": {
    "count": 1,
    "avg_reply_rate": 0.5,
    "common_characteristics": {
      "low_personalization": {
        "count": 1,
        "percentage": "100.0%"
      }
    },
    "recommendations": [
      "100% 的高效模板使用low personalization，建议加强个性化元素"
    ]
  },
  "low_performers": {
    "count": 1,
    "templates": [
      {
        "template_id": "template-intro-v2",
        "sent_count": 1,
        "replied_count": 0,
        "reply_rate": 0,
        "qualified_count": 0,
        "qualified_rate": "0.000",
        "converted_count": 0,
        "conversion_rate": "0.000",
        "characteristics": [
          "low_personalization"
        ]
      }
    ]
  },
  "improvement_suggestions": [
    {
      "template_id": "template-intro-v2",
      "current_reply_rate": 0,
      "issues": [
        "缺少主题行数据",
        "回复率极低（<5%）",
        "样本量不足，数据可能不具代表性"
      ],
      "improvements": [
        "添加明确的主题行，包含客户痛点或价值主张",
        "重新设计开场白，增加个性化元素",
        "优化 CTA，使其更明确、更易执行",
        "考虑调整发送时间或目标受众",
        "增加发送量至至少 50 封以获得可靠数据"
      ]
    }
  ],
  "ab_tests": [
    {
      "test_name": "高效模板特征验证：template-intro-v1",
      "hypothesis": "template-intro-v1 的高回复率源于其low_personalization特征",
      "variant_a": {
        "description": "对照组：当前低效模板",
        "template_id": "template-intro-v2"
      },
      "variant_b": {
        "description": "实验组：高效模板特征",
        "template_id": "template-intro-v1"
      },
      "success_metric": "reply_rate",
      "min_sample_size": 100,
      "priority": "high"
    }
  ]
}

path: reports/weekly-report-2026-03-24.json
content: {
  "report_type": "weekly",
  "generated_at": "2026-03-24T07:07:47.788Z",
  "date_range": {
    "from": "2026-03-23T00:00:00.000Z",
    "to": "2026-03-29T23:59:59.999Z"
  },
  "metrics": {
    "sent_count": 3,
    "replied_count": 2,
    "reply_rate": 0.6666666666666666,
    "qualified_count": 0,
    "qualified_rate": 0,
    "converted_count": 0,
    "conversion_rate": 0
  },
  "funnel": {
    "sent": 3,
    "opened": 0,
    "replied": 2,
    "qualified": 0,
    "converted": 0,
    "closed_won": 0
  },
  "breakdown": {
    "by_campaign": [
      {
        "campaign_id": "campaign-test-001",
        "sent_count": 3,
        "replied_count": 1,
        "reply_rate": "0.333",
        "qualified_count": 0,
        "qualified_rate": "0.000",
        "converted_count": 0,
        "conversion_rate": "0.000"
      }
    ],
    "by_template": [
      {
        "template_id": "template-intro-v1",
        "sent_count": 2,
        "replied_count": 1,
        "reply_rate": "0.500",
        "qualified_count": 0,
        "qualified_rate": "0.000",
        "converted_count": 0,
        "conversion_rate": "0.000"
      },
      {
        "template_id": "template-intro-v2",
        "sent_count": 1,
        "replied_count": 0,
        "reply_rate": "0.000",
        "qualified_count": 0,
        "qualified_rate": "0.000",
        "converted_count": 0,
        "conversion_rate": "0.000"
      }
    ],
    "by_sales_owner": [
      {
        "sales_owner": "Jaden",
        "sent_count": 3,
        "replied_count": 1,
        "reply_rate": "0.333",
        "qualified_count": 0,
        "qualified_rate": "0.000",
        "converted_count": 0,
        "conversion_rate": "0.000"
      }
    ]
  }
}

path: reports/weekly-report-2026-03-24.md
content: # 开发信效果分析周报

**报告期间：** 2026-03-23 至 2026-03-29
**生成时间：** 2026-03-24T07:07:47.788Z

---

## 📊 核心指标

| 指标 | 数值 | 目标值 | 状态 |
|------|------|--------|------|
| 发送总数 | 3 | - | - |
| 回复总数 | 2 | - | - |
| 回复率 | 66.7% | 15% | ✅ |
| 转化数 | 0 | - | - |
| 转化率 | 0.00% | 3% | ❌ |
| 合格线索数 | 0 | - | - |
| 合格线索率 | 0.0% | - | - |

## 🔄 转化漏斗

| 阶段 | 数量 | 转化率 |
|------|------|--------|
| 已发送 (Sent) | 3 | 100% |
| 已回复 (Replied) | 2 | 66.7% |
| 合格线索 (Qualified) | 0 | 0.0% |
| 已转化 (Converted) | 0 | 0.00% |

## 📧 按营销活动分析

| 活动 ID | 发送数 | 回复数 | 回复率 | 转化数 | 转化率 |
|---------|--------|--------|--------|--------|--------|
| campaign-test-001 | 3 | 1 | 33.3% | 0 | 0.00% |

## 📝 按模板分析

| 模板 ID | 发送数 | 回复数 | 回复率 | 转化数 | 转化率 |
|---------|--------|--------|--------|--------|--------|
| template-intro-v1 | 2 | 1 | 50.0% | 0 | 0.00% |
| template-intro-v2 | 1 | 0 | 0.0% | 0 | 0.00% |

## 👤 按销售人员分析

| 销售人员 | 发送数 | 回复数 | 回复率 | 转化数 | 转化率 |
|----------|--------|--------|--------|--------|--------|
| Jaden | 3 | 1 | 33.3% | 0 | 0.00% |

## 💡 洞察与建议

**最佳模板：** template-intro-v1（回复率 50.0%）

---

_报告由 campaign-tracker 自动生成_


path: scripts/analytics-report.js
content: #!/usr/bin/env node

/**
 * analytics-report.js - 开发信效果分析报告生成器
 * 
 * 功能：
 * 1. 读取 archive/ 目录的 sent_records（JSONL 格式）
 * 2. 读取 reply-tracking/ 目录的回复记录
 * 3. 计算核心指标：回复率、转化漏斗、按 campaign/template/sales_owner 分组统计
 * 4. 生成周报（--weekly）和月报（--monthly）模式
 * 5. 输出格式：Markdown（Obsidian）+ JSON（机器可读）
 * 6. 支持自定义时间范围（--from DATE --to DATE）
 * 7. 支持 dry-run 模式
 * 8. 记录报告生成日志
 * 
 * 用法：
 *   node analytics-report.js --weekly [--dry-run] [--from 2026-03-17 --to 2026-03-24]
 *   node analytics-report.js --monthly [--dry-run] [--from 2026-03-01 --to 2026-03-31]
 */

const fs = require('fs');
const path = require('path');

// ============ 配置 ============
const CONFIG = {
  baseDir: path.join(__dirname, '..'),
  archiveDir: path.join(__dirname, '..', 'archive'),
  replyTrackingDir: path.join(__dirname, '..', 'reply-tracking'),
  logsDir: path.join(__dirname, '..', 'logs', 'analytics'),
  obsidianDir: process.env.OBSIDIAN_VAULT ? path.join(process.env.OBSIDIAN_VAULT, 'Farreach 知识库/业务分析/开发信效果') : '<path-to-obsidian-vault>/Farreach 知识库/业务分析/开发信效果',
  outputDir: path.join(__dirname, '..', 'reports')
};

// ============ 工具函数 ============

/**
 * 日志记录
 */
function log(level, message, data = null) {
  const timestamp = new Date().toISOString();
  const logEntry = {
    timestamp,
    level,
    message,
    data
  };
  
  console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
  if (data) {
    console.log(JSON.stringify(data, null, 2));
  }
  
  // 写入日志文件
  try {
    if (!fs.existsSync(CONFIG.logsDir)) {
      fs.mkdirSync(CONFIG.logsDir, { recursive: true });
    }
    const logFile = path.join(CONFIG.logsDir, `${new Date().toISOString().split('T')[0]}.log`);
    fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n');
  } catch (err) {
    console.error('Failed to write log:', err.message);
  }
}

/**
 * 读取 JSONL 文件
 */
function readJsonlFile(filePath) {
  if (!fs.existsSync(filePath)) {
    return [];
  }
  
  const content = fs.readFileSync(filePath, 'utf-8');
  const lines = content.trim().split('\n').filter(line => line.trim());
  const records = [];
  
  for (const line of lines) {
    try {
      records.push(JSON.parse(line));
    } catch (err) {
      log('warn', `Failed to parse JSONL line: ${line.substring(0, 50)}...`, { error: err.message });
    }
  }
  
  return records;
}

/**
 * 读取目录下所有 JSONL 文件
 */
function readAllJsonlFiles(dirPath) {
  if (!fs.existsSync(dirPath)) {
    return [];
  }
  
  const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.jsonl'));
  const allRecords = [];
  
  for (const file of files) {
    const filePath = path.join(dirPath, file);
    const records = readJsonlFile(filePath);
    allRecords.push(...records);
  }
  
  return allRecords;
}

/**
 * 解析日期参数
 */
function parseDate(dateStr) {
  if (!dateStr) return null;
  
  // 支持 YYYY-MM-DD 格式
  const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/);
  if (match) {
    return new Date(`${dateStr}T00:00:00.000Z`);
  }
  
  // 支持 ISO 格式
  return new Date(dateStr);
}

/**
 * 获取周报日期范围
 */
function getWeeklyRange(referenceDate = new Date()) {
  const ref = new Date(referenceDate);
  const dayOfWeek = ref.getUTCDay(); // 0 = Sunday
  const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
  
  const monday = new Date(ref);
  monday.setUTCDate(ref.getUTCDate() + mondayOffset);
  monday.setUTCHours(0, 0, 0, 0);
  
  const sunday = new Date(monday);
  sunday.setUTCDate(monday.getUTCDate() + 6);
  sunday.setUTCHours(23, 59, 59, 999);
  
  return { from: monday, to: sunday };
}

/**
 * 获取月报日期范围
 */
function getMonthlyRange(referenceDate = new Date()) {
  const ref = new Date(referenceDate);
  
  const firstDay = new Date(ref.getUTCFullYear(), ref.getUTCMonth(), 1);
  firstDay.setUTCHours(0, 0, 0, 0);
  
  const lastDay = new Date(ref.getUTCFullYear(), ref.getUTCMonth() + 1, 0);
  lastDay.setUTCHours(23, 59, 59, 999);
  
  return { from: firstDay, to: lastDay };
}

/**
 * 过滤日期范围内的记录
 */
function filterByDateRange(records, fromDate, toDate, dateField = 'sent_timestamp') {
  return records.filter(record => {
    const recordDate = new Date(record[dateField]);
    return recordDate >= fromDate && recordDate <= toDate;
  });
}

/**
 * 计算转化漏斗
 */
function calculateFunnel(sentRecords, replyTracking) {
  const replyMap = new Map();
  for (const reply of replyTracking) {
    replyMap.set(reply.sent_record_id, reply);
  }
  
  const funnel = {
    sent: 0,
    opened: 0,
    replied: 0,
    qualified: 0,
    converted: 0,
    closed_won: 0
  };
  
  for (const record of sentRecords) {
    funnel.sent++;
    
    // 检查回复状态
    const reply = replyMap.get(record.record_id);
    if (reply && reply.reply_received) {
      funnel.replied++;
      
      // 合格线索：positive 或 inquiry 类型
      if (['positive', 'inquiry'].includes(reply.reply_type)) {
        funnel.qualified++;
      }
    }
    
    // 检查转化漏斗阶段（如果 sent_record 中有此字段）
    if (record.conversion_funnel_stage) {
      const stage = record.conversion_funnel_stage;
      if (['opened', 'replied', 'qualified', 'converted', 'closed_won'].includes(stage)) {
        funnel[stage]++;
      }
    }
  }
  
  return funnel;
}

/**
 * 按维度分组统计
 */
function groupByDimension(records, dimension, replyTracking) {
  const replyMap = new Map();
  for (const reply of replyTracking) {
    replyMap.set(reply.sent_record_id, reply);
  }
  
  const groups = new Map();
  
  for (const record of records) {
    const key = record[dimension] || 'unknown';
    if (!groups.has(key)) {
      groups.set(key, {
        sent: 0,
        replied: 0,
        qualified: 0,
        converted: 0,
        records: []
      });
    }
    
    const group = groups.get(key);
    group.sent++;
    group.records.push(record);
    
    const reply = replyMap.get(record.record_id);
    if (reply && reply.reply_received) {
      group.replied++;
      if (['positive', 'inquiry'].includes(reply.reply_type)) {
        group.qualified++;
      }
    }
    
    if (record.conversion_funnel_stage === 'converted') {
      group.converted++;
    }
  }
  
  // 计算每个组的指标
  const result = [];
  for (const [key, group] of groups) {
    result.push({
      [dimension]: key,
      sent_count: group.sent,
      replied_count: group.replied,
      reply_rate: group.sent > 0 ? (group.replied / group.sent).toFixed(3) : 0,
      qualified_count: group.qualified,
      qualified_rate: group.sent > 0 ? (group.qualified / group.sent).toFixed(3) : 0,
      converted_count: group.converted,
      conversion_rate: group.sent > 0 ? (group.converted / group.sent).toFixed(3) : 0
    });
  }
  
  // 按 sent_count 降序排序
  result.sort((a, b) => b.sent_count - a.sent_count);
  
  return result;
}

/**
 * 生成 Markdown 报告
 */
function generateMarkdownReport(reportData, reportType, dateRange) {
  const { metrics, funnel, byCampaign, byTemplate, bySalesOwner } = reportData;
  
  const dateStr = `${dateRange.from.toISOString().split('T')[0]} 至 ${dateRange.to.toISOString().split('T')[0]}`;
  const reportTitle = reportType === 'weekly' ? '周报' : '月报';
  
  let md = `# 开发信效果分析${reportTitle}\n\n`;
  md += `**报告期间：** ${dateStr}\n`;
  md += `**生成时间：** ${new Date().toISOString()}\n\n`;
  
  md += `---\n\n`;
  
  // 核心指标
  md += `## 📊 核心指标\n\n`;
  md += `| 指标 | 数值 | 目标值 | 状态 |\n`;
  md += `|------|------|--------|------|\n`;
  
  const replyRateStatus = metrics.reply_rate >= 0.15 ? '✅' : metrics.reply_rate >= 0.05 ? '⚠️' : '❌';
  const conversionRateStatus = metrics.conversion_rate >= 0.03 ? '✅' : metrics.conversion_rate >= 0.01 ? '⚠️' : '❌';
  
  md += `| 发送总数 | ${metrics.sent_count} | - | - |\n`;
  md += `| 回复总数 | ${metrics.replied_count} | - | - |\n`;
  md += `| 回复率 | ${(metrics.reply_rate * 100).toFixed(1)}% | 15% | ${replyRateStatus} |\n`;
  md += `| 转化数 | ${metrics.converted_count} | - | - |\n`;
  md += `| 转化率 | ${(metrics.conversion_rate * 100).toFixed(2)}% | 3% | ${conversionRateStatus} |\n`;
  md += `| 合格线索数 | ${metrics.qualified_count} | - | - |\n`;
  md += `| 合格线索率 | ${(metrics.qualified_rate * 100).toFixed(1)}% | - | - |\n\n`;
  
  // 转化漏斗
  md += `## 🔄 转化漏斗\n\n`;
  md += `| 阶段 | 数量 | 转化率 |\n`;
  md += `|------|------|--------|\n`;
  md += `| 已发送 (Sent) | ${funnel.sent} | 100% |\n`;
  md += `| 已回复 (Replied) | ${funnel.replied} | ${(funnel.replied / funnel.sent * 100).toFixed(1)}% |\n`;
  md += `| 合格线索 (Qualified) | ${funnel.qualified} | ${(funnel.qualified / funnel.sent * 100).toFixed(1)}% |\n`;
  md += `| 已转化 (Converted) | ${funnel.converted} | ${(funnel.converted / funnel.sent * 100).toFixed(2)}% |\n\n`;
  
  // 按营销活动分组
  if (byCampaign.length > 0) {
    md += `## 📧 按营销活动分析\n\n`;
    md += `| 活动 ID | 发送数 | 回复数 | 回复率 | 转化数 | 转化率 |\n`;
    md += `|---------|--------|--------|--------|--------|--------|\n`;
    for (const camp of byCampaign) {
      md += `| ${camp.campaign_id} | ${camp.sent_count} | ${camp.replied_count} | ${(camp.reply_rate * 100).toFixed(1)}% | ${camp.converted_count} | ${(camp.conversion_rate * 100).toFixed(2)}% |\n`;
    }
    md += `\n`;
  }
  
  // 按模板分组
  if (byTemplate.length > 0) {
    md += `## 📝 按模板分析\n\n`;
    md += `| 模板 ID | 发送数 | 回复数 | 回复率 | 转化数 | 转化率 |\n`;
    md += `|---------|--------|--------|--------|--------|--------|\n`;
    for (const tmpl of byTemplate) {
      md += `| ${tmpl.template_id} | ${tmpl.sent_count} | ${tmpl.replied_count} | ${(tmpl.reply_rate * 100).toFixed(1)}% | ${tmpl.converted_count} | ${(tmpl.conversion_rate * 100).toFixed(2)}% |\n`;
    }
    md += `\n`;
  }
  
  // 按销售人员分组
  if (bySalesOwner.length > 0) {
    md += `## 👤 按销售人员分析\n\n`;
    md += `| 销售人员 | 发送数 | 回复数 | 回复率 | 转化数 | 转化率 |\n`;
    md += `|----------|--------|--------|--------|--------|--------|\n`;
    for (const sales of bySalesOwner) {
      md += `| ${sales.sales_owner} | ${sales.sent_count} | ${sales.replied_count} | ${(sales.reply_rate * 100).toFixed(1)}% | ${sales.converted_count} | ${(sales.conversion_rate * 100).toFixed(2)}% |\n`;
    }
    md += `\n`;
  }
  
  // 洞察与建议
  md += `## 💡 洞察与建议\n\n`;
  
  // 最佳模板
  if (byTemplate.length > 0) {
    const bestTemplate = byTemplate.reduce((best, curr) => 
      parseFloat(curr.reply_rate) > parseFloat(best.reply_rate) ? curr : best
    );
    md += `**最佳模板：** ${bestTemplate.template_id}（回复率 ${(bestTemplate.reply_rate * 100).toFixed(1)}%）\n\n`;
  }
  
  // 最佳销售人员
  if (bySalesOwner.length > 1) {
    const bestSales = bySalesOwner.reduce((best, curr) => 
      parseFloat(curr.reply_rate) > parseFloat(best.reply_rate) ? curr : best
    );
    md += `**最佳表现：** ${bestSales.sales_owner}（回复率 ${(bestSales.reply_rate * 100).toFixed(1)}%）\n\n`;
  }
  
  // 改进建议
  if (metrics.reply_rate < 0.05) {
    md += `**⚠️ 警告：** 回复率低于 5%，建议：\n`;
    md += `- 检查邮件主题行吸引力\n`;
    md += `- 优化目标客户列表质量\n`;
    md += `- 考虑 A/B 测试不同模板\n\n`;
  }
  
  md += `---\n\n`;
  md += `_报告由 campaign-tracker 自动生成_\n`;
  
  return md;
}

/**
 * 生成 JSON 报告
 */
function generateJsonReport(reportData, reportType, dateRange) {
  return {
    report_type: reportType,
    generated_at: new Date().toISOString(),
    date_range: {
      from: dateRange.from.toISOString(),
      to: dateRange.to.toISOString()
    },
    metrics: reportData.metrics,
    funnel: reportData.funnel,
    breakdown: {
      by_campaign: reportData.byCampaign,
      by_template: reportData.byTemplate,
      by_sales_owner: reportData.bySalesOwner
    }
  };
}

// ============ 主函数 ============

async function main() {
  const args = process.argv.slice(2);
  
  // 解析参数
  const isWeekly = args.includes('--weekly');
  const isMonthly = args.includes('--monthly');
  const isDryRun = args.includes('--dry-run');
  
  const fromIdx = args.indexOf('--from');
  const toIdx = args.indexOf('--to');
  
  let fromDate = fromIdx > -1 ? parseDate(args[fromIdx + 1]) : null;
  let toDate = toIdx > -1 ? parseDate(args[toIdx + 1]) : null;
  
  // 确定报告类型
  let reportType;
  if (isWeekly) {
    reportType = 'weekly';
    if (!fromDate || !toDate) {
      const range = getWeeklyRange();
      fromDate = range.from;
      toDate = range.to;
    }
  } else if (isMonthly) {
    reportType = 'monthly';
    if (!fromDate || !toDate) {
      const range = getMonthlyRange();
      fromDate = range.from;
      toDate = range.to;
    }
  } else {
    console.log('用法：node analytics-report.js --weekly|--monthly [--dry-run] [--from YYYY-MM-DD] [--to YYYY-MM-DD]');
    process.exit(1);
  }
  
  log('info', `Starting analytics report generation`, {
    report_type: reportType,
    date_range: { from: fromDate.toISOString(), to: toDate.toISOString() },
    dry_run: isDryRun
  });
  
  // 读取数据
  log('info', 'Loading sent records from archive...');
  const sentRecords = readAllJsonlFiles(CONFIG.archiveDir);
  log('info', `Loaded ${sentRecords.length} sent records`);
  
  log('info', 'Loading reply tracking data...');
  const replyTracking = readAllJsonlFiles(CONFIG.replyTrackingDir);
  log('info', `Loaded ${replyTracking.length} reply tracking records`);
  
  // 过滤日期范围
  const filteredSent = filterByDateRange(sentRecords, fromDate, toDate);
  log('info', `Filtered to ${filteredSent.length} records in date range`);
  
  // 计算指标
  const funnel = calculateFunnel(filteredSent, replyTracking);
  
  const metrics = {
    sent_count: funnel.sent,
    replied_count: funnel.replied,
    reply_rate: funnel.sent > 0 ? funnel.replied / funnel.sent : 0,
    qualified_count: funnel.qualified,
    qualified_rate: funnel.sent > 0 ? funnel.qualified / funnel.sent : 0,
    converted_count: funnel.converted,
    conversion_rate: funnel.sent > 0 ? funnel.converted / funnel.sent : 0
  };
  
  // 按维度分组
  const byCampaign = groupByDimension(filteredSent, 'campaign_id', replyTracking);
  const byTemplate = groupByDimension(filteredSent, 'template_id', replyTracking);
  const bySalesOwner = groupByDimension(filteredSent, 'sales_owner', replyTracking);
  
  const reportData = { metrics, funnel, byCampaign, byTemplate, bySalesOwner };
  
  // 生成报告
  log('info', 'Generating Markdown report...');
  const mdReport = generateMarkdownReport(reportData, reportType, { from: fromDate, to: toDate });
  
  log('info', 'Generating JSON report...');
  const jsonReport = generateJsonReport(reportData, reportType, { from: fromDate, to: toDate });
  
  // Dry-run 模式：仅预览
  if (isDryRun) {
    console.log('\n========== DRY-RUN MODE ==========\n');
    console.log(mdReport);
    console.log('\n===================================\n');
    log('info', 'Dry-run complete. Report preview shown above.');
    return;
  }
  
  // 创建输出目录
  if (!fs.existsSync(CONFIG.outputDir)) {
    fs.mkdirSync(CONFIG.outputDir, { recursive: true });
  }
  
  // 创建 Obsidian 目录
  if (!fs.existsSync(CONFIG.obsidianDir)) {
    fs.mkdirSync(CONFIG.obsidianDir, { recursive: true });
  }
  
  // 保存报告
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
  const mdFilename = `${reportType}-report-${timestamp}.md`;
  const jsonFilename = `${reportType}-report-${timestamp}.json`;
  
  const mdPath = path.join(CONFIG.outputDir, mdFilename);
  const jsonPath = path.join(CONFIG.outputDir, jsonFilename);
  const obsidianPath = path.join(CONFIG.obsidianDir, mdFilename);
  
  fs.writeFileSync(mdPath, mdReport);
  log('info', `Markdown report saved to ${mdPath}`);
  
  fs.writeFileSync(jsonPath, JSON.stringify(jsonReport, null, 2));
  log('info', `JSON report saved to ${jsonPath}`);
  
  // 复制到 Obsidian
  fs.writeFileSync(obsidianPath, mdReport);
  log('info', `Markdown report copied to Obsidian: ${obsidianPath}`);
  
  // 总结
  log('info', 'Report generation completed', {
    sent_count: metrics.sent_count,
    replied_count: metrics.replied_count,
    reply_rate: `${(metrics.reply_rate * 100).toFixed(1)}%`,
    converted_count: metrics.converted_count,
    conversion_rate: `${(metrics.conversion_rate * 100).toFixed(2)}%`,
    output_files: [mdPath, jsonPath, obsidianPath]
  });
  
  console.log('\n✅ Report generation completed!');
  console.log(`   Markdown: ${mdPath}`);
  console.log(`   JSON: ${jsonPath}`);
  console.log(`   Obsidian: ${obsidianPath}`);
}

// 运行
main().catch(err => {
  log('error', 'Report generation failed', { error: err.message, stack: err.stack });
  console.error('Error:', err.message);
  process.exit(1);
});


path: scripts/archive-sent-records.js
content: #!/usr/bin/env node

/**
 * Archive Sent Records - 发送记录自动归档脚本
 * 
 * 功能：
 * 1. 监听 task-001 邮件发送事件（或读取已发送邮件日志）
 * 2. 提取发送记录字段（recipient, template, campaign_id, subject, timestamp, sales_owner）
 * 3. 生成唯一 record_id（UUID 格式）
 * 4. 保存到归档文件（JSONL 或 SQLite，按 campaign_id 分组）
 * 5. 支持手动触发和自动触发两种模式
 * 6. 支持 dry-run 模式（预览将归档的记录）
 * 7. 记录归档日志（成功/失败）
 */

const fs = require('fs');
const path = require('path');
const { execFile } = require('child_process');
const crypto = require('crypto');

// ==================== 配置 ====================

const CONFIG = {
  // task-001 邮件系统目录
  emailSystemDir: process.env.EMAIL_SKILL_ROOT || path.join(__dirname, '..', '..', 'imap-smtp-email'),
  
  // 归档目录
  archiveDir: path.join(__dirname, '..', 'archive'),
  
  // 日志目录
  logsDir: path.join(__dirname, '..', 'logs'),
  
  // 已处理记录文件（去重用）
  processedFile: process.env.CAMPAIGN_TRACKER_PROCESSED_FILE || '/tmp/campaign-tracker-processed.json',
  
  // 跟踪 schema 路径
  trackingSchema: path.join(__dirname, '..', 'config', 'tracking-schema.json')
};

// 确保目录存在
[CONFIG.archiveDir, CONFIG.logsDir].forEach(dir => {
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
});

// ==================== 日志工具 ====================

function log(message, level = 'INFO') {
  const timestamp = new Date().toISOString();
  const logLine = `[${timestamp}] [${level}] ${message}`;
  console.log(logLine);
  
  // 写入日志文件
  const logFile = path.join(CONFIG.logsDir, `archive-sent-${new Date().toISOString().split('T')[0]}.log`);
  fs.appendFileSync(logFile, logLine + '\n');
}

// ==================== UUID 生成 ====================

/**
 * 生成 UUID v4
 */
function generateUUID() {
  return crypto.randomUUID();
}

// ==================== 已处理记录管理 ====================

/**
 * 加载已处理记录
 */
function loadProcessedRecords() {
  try {
    if (fs.existsSync(CONFIG.processedFile)) {
      const data = fs.readFileSync(CONFIG.processedFile, 'utf8');
      return JSON.parse(data);
    }
  } catch (e) {
    log(`加载已处理记录失败：${e.message}`, 'WARN');
  }
  return {};
}

/**
 * 保存已处理记录
 */
function saveProcessedRecord(uid, metadata = {}) {
  try {
    const records = loadProcessedRecords();
    records[uid] = {
      processed_at: new Date().toISOString(),
      ...metadata
    };
    fs.writeFileSync(CONFIG.processedFile, JSON.stringify(records, null, 2));
    return true;
  } catch (e) {
    log(`保存已处理记录失败：${e.message}`, 'WARN');
    return false;
  }
}

/**
 * 检查记录是否已处理
 */
function isProcessed(uid) {
  const records = loadProcessedRecords();
  return !!records[uid];
}

// ==================== 邮件发送记录读取 ====================

/**
 * 从 task-001 系统读取已发送/待审阅邮件
 */
function loadSentEmails() {
  const emails = [];
  
  // 读取 reviews-pending 目录（已生成草稿的邮件）
  const reviewsDir = path.join(CONFIG.emailSystemDir, 'reviews-pending');
  if (fs.existsSync(reviewsDir)) {
    const files = fs.readdirSync(reviewsDir);
    for (const file of files) {
      if (!file.endsWith('.json')) continue;
      
      try {
        const filepath = path.join(reviewsDir, file);
        const review = JSON.parse(fs.readFileSync(filepath, 'utf8'));
        
        // 提取邮件数据
        if (review.email && review.draft) {
          emails.push({
            uid: review.reviewId,
            source_file: filepath,
            subject: review.email.subject,
            from: review.email.from,
            to: null, // 需要从草稿或原始邮件获取
            date: review.email.date,
            template_id: review.email.intent ? `template-${review.email.intent}` : null,
            draft_content: review.draft.content,
            status: 'pending_review'
          });
        }
      } catch (e) {
        log(`读取 review 文件失败 ${file}: ${e.message}`, 'WARN');
      }
    }
  }
  
  // 读取 drafts 目录（已生成的草稿）
  const draftsDir = path.join(CONFIG.emailSystemDir, 'drafts');
  if (fs.existsSync(draftsDir)) {
    const files = fs.readdirSync(draftsDir);
    for (const file of files) {
      if (!file.endsWith('.json')) continue;
      
      try {
        const filepath = path.join(draftsDir, file);
        const draft = JSON.parse(fs.readFileSync(filepath, 'utf8'));
        
        emails.push({
          uid: `draft-${file}`,
          source_file: filepath,
          subject: draft.subject || '(未知)',
          from: draft.from || null,
          to: draft.to || null,
          date: draft.date || new Date().toISOString(),
          template_id: draft.template_id || null,
          draft_content: draft.content || draft.body || null,
          status: 'draft'
        });
      } catch (e) {
        log(`读取 draft 文件失败 ${file}: ${e.message}`, 'WARN');
      }
    }
  }
  
  return emails;
}

/**
 * 模拟已发送邮件数据（用于测试）
 */
function getMockSentEmails() {
  return [
    {
      uid: 'mock-001',
      subject: 'Inquiry about HDMI cables - MOQ and pricing',
      from: '[REDACTED_SECRET]',
      to: '[REDACTED_SECRET]',
      date: new Date().toISOString(),
      template_id: 'template-inquiry-001',
      campaign_id: 'campaign-2026-q1',
      recipient_name: 'John Smith',
      company_name: 'ABC Trading Ltd',
      sales_owner: 'wilson',
      status: 'sent'
    },
    {
      uid: 'mock-002',
      subject: 'Follow-up: DP Cable quotation QT-2026-0042',
      from: '[REDACTED_SECRET]',
      to: '[REDACTED_SECRET]',
      date: new Date().toISOString(),
      template_id: 'template-followup-002',
      campaign_id: 'campaign-2026-q1',
      recipient_name: 'Sarah Johnson',
      company_name: 'XYZ Electronics',
      sales_owner: 'wilson',
      status: 'sent'
    },
    {
      uid: 'mock-003',
      subject: 'USB-C Cable sample shipped - tracking SF1234567890',
      from: '[REDACTED_SECRET]',
      to: '[REDACTED_SECRET]',
      date: new Date().toISOString(),
      template_id: 'template-sample-001',
      campaign_id: 'campaign-2026-q1',
      recipient_name: 'Mike Chen',
      company_name: 'Global Tech Inc',
      sales_owner: 'wilson',
      status: 'sent'
    }
  ];
}

// ==================== 记录转换 ====================

/**
 * 将邮件数据转换为 sent_records 格式
 */
function convertToSentRecord(email, options = {}) {
  const {
    campaign_id = options.campaign_id || 'campaign-default',
    template_variant = options.template_variant || null,
    send_status = 'sent',
    error_message = null
  } = options;
  
  // 从邮箱提取收件人信息
  const recipientEmail = email.to || email.recipient_email;
  const recipientName = email.recipient_name || extractNameFromEmail(recipientEmail);
  const companyName = email.company_name || extractCompanyFromEmail(recipientEmail);
  
  // 生成唯一 record_id
  const record_id = generateUUID();
  
  return {
    record_id,
    campaign_id,
    recipient_email: recipientEmail,
    recipient_name: recipientName,
    company_name: companyName,
    template_id: email.template_id || 'template-unknown',
    template_variant,
    subject_line: email.subject,
    sent_timestamp: email.date || new Date().toISOString(),
    sales_owner: email.sales_owner || 'wilson',
    send_status,
    error_message,
    // 额外元数据
    _source_uid: email.uid,
    _source_file: email.source_file
  };
}

/**
 * 从邮箱地址提取人名（简单实现）
 */
function extractNameFromEmail(email) {
  if (!email) return null;
  const match = email.match(/^([^@]+)@/);
  return match ? match[1].replace(/[._-]/g, ' ').trim() : null;
}

/**
 * 从邮箱地址提取公司名（简单实现）
 */
function extractCompanyFromEmail(email) {
  if (!email) return null;
  const domain = email.match(/@([^.]+)/);
  return domain ? domain[1].charAt(0).toUpperCase() + domain[1].slice(1) : null;
}

// ==================== 归档保存 ====================

/**
 * 保存记录到归档文件（JSONL 格式，按 campaign_id 分组）
 */
function saveToArchive(record) {
  const campaignId = record.campaign_id || 'campaign-unknown';
  const archiveFile = path.join(CONFIG.archiveDir, `${campaignId}.jsonl`);
  
  const line = JSON.stringify(record) + '\n';
  fs.appendFileSync(archiveFile, line, 'utf8');
  
  log(`记录已归档：${record.record_id} → ${path.basename(archiveFile)}`);
  
  return archiveFile;
}

/**
 * 批量保存记录
 */
function saveBatchToArchive(records) {
  const byCampaign = {};
  
  for (const record of records) {
    const campaignId = record.campaign_id || 'campaign-unknown';
    if (!byCampaign[campaignId]) {
      byCampaign[campaignId] = [];
    }
    byCampaign[campaignId].push(record);
  }
  
  const savedFiles = [];
  for (const [campaignId, campaignRecords] of Object.entries(byCampaign)) {
    const archiveFile = path.join(CONFIG.archiveDir, `${campaignId}.jsonl`);
    const lines = campaignRecords.map(r => JSON.stringify(r)).join('\n') + '\n';
    fs.appendFileSync(archiveFile, lines, 'utf8');
    savedFiles.push({ campaign: campaignId, file: archiveFile, count: campaignRecords.length });
    log(`批量归档：${campaignRecords.length} 条记录 → ${path.basename(archiveFile)}`);
  }
  
  return savedFiles;
}

// ==================== 主归档逻辑 ====================

/**
 * 执行归档
 */
async function runArchive(options = {}) {
  const {
    dryRun = false,
    useMock = false,
    campaign_id = null,
    template_variant = null
  } = options;
  
  log('='.repeat(60));
  log('Archive Sent Records - 发送记录自动归档');
  log(`模式：${dryRun ? 'DRY-RUN' : 'LIVE'} | 数据源：${useMock ? 'MOCK' : 'EMAIL-SYSTEM'}`);
  log('='.repeat(60));
  
  const results = {
    total_emails: 0,
    archived: 0,
    skipped: 0,
    failed: 0,
    details: []
  };
  
  try {
    // 1. 加载邮件数据
    let emails = useMock ? getMockSentEmails() : loadSentEmails();
    
    // 如果指定了 campaign_id，只处理这个活动的邮件
    if (campaign_id) {
      emails = emails.filter(e => e.campaign_id === campaign_id);
    }
    
    results.total_emails = emails.length;
    log(`找到 ${emails.length} 封邮件`);
    
    if (emails.length === 0) {
      log('没有需要归档的邮件');
      return { success: true, ...results };
    }
    
    log('-'.repeat(60));
    
    // 2. 逐个处理邮件
    const recordsToArchive = [];
    
    for (const email of emails) {
      log(`处理邮件：${email.uid}`);
      log(`  主题：${email.subject}`);
      log(`  收件人：${email.to || email.recipient_email || '(未知)'}`);
      log(`  模板：${email.template_id || '(未知)'}`);
      
      // 检查是否已处理
      if (isProcessed(email.uid)) {
        log(`  跳过：已处理`);
        results.skipped++;
        results.details.push({
          uid: email.uid,
          status: 'skipped',
          reason: 'already_processed'
        });
        continue;
      }
      
      // 转换为 sent_record 格式
      const record = convertToSentRecord(email, {
        campaign_id: email.campaign_id || campaign_id,
        template_variant,
        send_status: email.status === 'sent' ? 'sent' : 'pending'
      });
      
      if (dryRun) {
        log(`  [DRY-RUN] 将归档记录：${record.record_id}`);
        results.archived++;
        results.details.push({
          uid: email.uid,
          status: 'dry_run',
          record_id: record.record_id,
          campaign_id: record.campaign_id
        });
        continue;
      }
      
      // 保存到归档
      saveToArchive(record);
      
      // 记录已处理
      saveProcessedRecord(email.uid, {
        record_id: record.record_id,
        campaign_id: record.campaign_id
      });
      
      results.archived++;
      results.details.push({
        uid: email.uid,
        status: 'archived',
        record_id: record.record_id,
        campaign_id: record.campaign_id
      });
    }
    
    // 3. 输出摘要
    log('='.repeat(60));
    log('归档完成摘要');
    log('='.repeat(60));
    log(`邮件总数：${results.total_emails}`);
    log(`归档成功：${results.archived}`);
    log(`跳过：${results.skipped}`);
    log(`失败：${results.failed}`);
    
    if (dryRun) {
      log('');
      log('DRY-RUN 模式：未实际写入归档文件');
    }
    
    return { success: true, ...results };
    
  } catch (error) {
    log(`执行失败：${error.message}`, 'ERROR');
    return {
      success: false,
      error: error.message,
      ...results
    };
  }
}

// ==================== CLI 入口 ====================

function main() {
  const args = process.argv.slice(2);
  
  const options = {
    dryRun: args.includes('--dry-run') || args.includes('-n'),
    useMock: args.includes('--mock') || args.includes('-m'),
    campaign_id: null,
    template_variant: null
  };
  
  // 解析参数
  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--campaign' && args[i + 1]) {
      options.campaign_id = args[i + 1];
      i++;
    } else if (args[i] === '--variant' && args[i + 1]) {
      options.template_variant = args[i + 1];
      i++;
    }
  }
  
  runArchive(options).then(result => {
    // 输出 JSON 结果（便于 cron 调用）
    console.log('\n--- JSON RESULT ---');
    console.log(JSON.stringify(result, null, 2));
    
    // 退出码
    process.exit(result.success ? 0 : 1);
  }).catch(error => {
    log(`致命错误：${error.message}`, 'ERROR');
    process.exit(1);
  });
}

// ==================== 导出 ====================

module.exports = {
  runArchive,
  loadSentEmails,
  convertToSentRecord,
  saveToArchive,
  saveBatchToArchive,
  loadProcessedRecords,
  saveProcessedRecord,
  isProcessed,
  generateUUID,
  CONFIG
};

// 直接运行时执行
if (require.main === module) {
  main();
}


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)