Skill: cjboy007/follow-up-engine (version 1.0.0)

--- name: follow-up-engine description: "Automated customer follow-up scheduling and execution engine for B2B sales. Generates personalized follow-up email drafts based on customer stage, last contact date, and follow-up strategy. Integrates with CRM systems (configurable) to sync follow-up records. Use when you need to automate outbound sales follow-ups, schedule reminders, or generate follow-up email content for dormant leads." --- # Follow-up Engine - OpenClaw Skill 定义 ## 技能描述 自动化客户询盘跟进规则引擎，基于配置的时间触发器和条件判断，智能生成跟进邮件草稿并同步到 OKKI CRM。 --- ## 触发条件 ### 定时触发（Cron） ```bash # 每 30 分钟执行一次检测 */30 * * * * cd $WORKSPACE/skills/follow-up-engine && node scripts/follow-up-scheduler.js --mode auto >> /tmp/follow-up-scheduler.log 2>&1 # 每小时执行一次 OKKI 同步 0 * * * * cd $WORKSPACE/skills/follow-up-engine && node scripts/okki-integration.js --sync >> /tmp/okki-integration.log 2>&1 ``` ### 事件触发 - **新询盘邮件到达** → 触发 new_inquiry 阶段跟进流程 - **报价单发送完成** → 触发 quoted 阶段跟进序列 - **样品寄送确认** → 触发 sample_sent 阶段跟进 - **OKKI 跟进记录创建** → 触发阶段流转检测 --- ## 执行逻辑 ### 1. Follow-up Scheduler 流程 ``` 1. 加载配置 ↓ 2. 扫描 OKKI 客户列表 ↓ 3. 识别每个客户当前阶段 ↓ 4. 计算下次跟进时间 ↓ 5. 匹配到期需要跟进的客户 ↓ 6. 生成跟进邮件草稿 ↓ 7. 保存到 drafts/ 目录 ↓ 8. 记录执行日志 ``` ### 2. OKKI Integration 流程 ``` 1. 读取 drafts/ 目录草稿 ↓ 2. 过滤已同步草稿（status != synced） ↓ 3. 匹配 OKKI 客户（域名搜索 + 名称搜索） ↓ 4. 调用 OKKI API 创建跟进记录 ↓ 5. 更新草稿状态为 synced ↓ 6. 记录同步日志 ``` --- ## 输入输出 ### 输入 | 来源 | 数据类型 | 说明 | |------|---------|------| | OKKI API | 客户列表 | 客户邮箱、公司名称、阶段 | | follow-up-strategies.json | 配置 | 跟进序列、模板、升级规则 | | task-001 email-smart-reply | 模板系统 | 邮件模板内容 | ### 输出 | 目标 | 数据类型 | 说明 | |------|---------|------| | drafts/ | JSON 文件 | 跟进邮件草稿 | | OKKI API | 跟进记录 | trail_type=105 | | logs/ | 日志文件 | 执行记录、错误信息 | | Discord | 通知消息 | 待审阅草稿提醒 | --- ## 配置项 ### 环境变量 ```bash # OKKI CLI 路径 export OKKI_CLI_PATH="$WORKSPACE/xiaoman-okki/api/okki.py" # Discord Bot Token（通知用） export DISCORD_BOT_TOKEN="your-discord-bot-token-here" # 通知频道 ID export DISCORD_CHANNEL_ID="<your-discord-channel-id>" ``` ### 配置文件 | 文件 | 用途 | 必填 | |------|------|------| | config/follow-up-rules.json | 跟进规则定义 | ✅ | | config/stage-transitions.json | 阶段流转模型 | ✅ | | config/follow-up-strategies.json | 跟进策略模板 | ✅ | --- ## CLI 命令 ### follow-up-scheduler.js ```bash # Dry-run 模式（预览） node scripts/follow-up-scheduler.js --dry-run # 自动模式（定时触发） node scripts/follow-up-scheduler.js --mode auto # 手动模式（立即执行） node scripts/follow-up-scheduler.js --mode manual # 调试模式（详细日志） node scripts/follow-up-scheduler.js --debug # 强制更新阶段 node scripts/follow-up-scheduler.js --force-stage-update ``` ### okki-integration.js ```bash # Dry-run 模式（预览） node scripts/okki-integration.js --dry-run # 同步模式（实际创建 OKKI 记录） node scripts/okki-integration.js --sync # 批量同步（指定草稿文件） node scripts/okki-integration.js --batch drafts/*.json # 查看同步日志 node scripts/okki-integration.js --logs ``` --- ## 依赖关系 ### 上游依赖 | Skill | 用途 | 必须 | |-------|------|------| | email-smart-reply (task-001) | 邮件模板系统 | ✅ | | okki-email-sync (task-002) | OKKI API 封装 | ✅ | ### 下游集成 | 目标 | 用途 | 必须 | |------|------|------| | OKKI CRM | 客户数据 + 跟进记录 | ✅ | | Discord | 审阅通知 | ✅ | | Obsidian | 日志归档 | ❌ | --- ## 错误处理 ### 常见错误码 | 错误 | 原因 | 解决方案 | |------|------|---------| | CONFIG_NOT_FOUND | 配置文件缺失 | 检查 config/ 目录 | | OKKI_AUTH_FAILED | OKKI 认证失败 | 检查 okki.py 配置 | | NO_CUSTOMERS_FOUND | 未找到客户 | 检查 OKKI 连接 | | TEMPLATE_NOT_FOUND | 模板不存在 | 检查 email-smart-reply | ### 降级策略 1. **OKKI 不可用** → 使用本地缓存客户列表 2. **模板不可用** → 使用默认跟进模板 3. **Discord 不可用** → 日志记录 + 本地通知文件 --- ## 测试 ### 单元测试 ```bash # 测试配置文件 node -e "JSON.parse(require('fs').readFileSync('config/follow-up-rules.json'))" && echo "✓ follow-up-rules.json valid" node -e "JSON.parse(require('fs').readFileSync('config/stage-transitions.json'))" && echo "✓ stage-transitions.json valid" node -e "JSON.parse(require('fs').readFileSync('config/follow-up-strategies.json'))" && echo "✓ follow-up-strategies.json valid" ``` ### 端到端测试 ```bash # 运行 E2E 测试 bash test/e2e.sh # 预期输出： # ✓ 配置文件验证通过 # ✓ 模拟客户创建成功 # ✓ 草稿生成成功（3 封） # ✓ OKKI 同步成功（dry-run） # ✓ 日志记录完整 ``` --- ## 性能指标 | 指标 | 目标值 | 实测值 | |------|--------|--------| | 客户扫描速度 | > 100 客户/秒 | TBD | | 草稿生成时间 | < 1 秒/草稿 | TBD | | OKKI 同步速度 | > 10 记录/秒 | TBD | | 内存占用 | < 100MB | TBD | --- ## 安全规范 - ✅ 所有配置文件使用 JSON 格式（无代码注入风险） - ✅ OKKI API 调用使用官方 CLI（不直接暴露 API Key） - ✅ 日志脱敏处理（隐藏客户邮箱等敏感信息） - ✅ 发送前必须人工审阅（严禁自动盲发） --- **Skill 版本**: 1.0 **创建日期**: 2026-03-24 **维护者**: Wilson Evolution System

path: README.md
content: # Follow-up Engine - 询盘跟进自动化引擎

🧠 **Wilson Evolution Phase 2 Skill**

自动化客户询盘跟进流程，基于规则引擎智能触发跟进邮件，集成 OKKI CRM 系统。

---

## 📋 功能概述

- **6 阶段客户生命周期管理**：新询盘 → 已报价 → 已样品 → 谈判中 → 成交 → 流失
- **智能规则引擎**：时间触发 + 条件判断 + 自动升级
- **自动草稿生成**：基于客户阶段自动生成个性化跟进邮件
- **OKKI 双向同步**：自动创建跟进记录，保持 CRM 数据最新
- **Discord 通知**：关键节点实时推送

---

## 📁 目录结构

```
follow-up-engine/
├── README.md              # 本文件
├── SKILL.md               # OpenClaw skill 定义
├── config/
│   ├── follow-up-rules.json      # 跟进规则配置（6 阶段、10 规则）
│   ├── stage-transitions.json    # 阶段流转模型（12 流转规则）
│   └── follow-up-strategies.json # 跟进策略模板（24 模板）
├── scripts/
│   ├── follow-up-scheduler.js    # 定时检测 + 草稿生成（400+ 行）
│   └── okki-integration.js       # OKKI 跟进记录集成（600+ 行）
├── drafts/                # 生成的跟进草稿
├── logs/                  # 运行日志
└── test/
    └── e2e.sh             # 端到端测试脚本
```

---

## 🚀 快速开始

### 1. 前置依赖

确保以下 skills 已安装并配置：
- `email-smart-reply` (task-001) - 邮件 intent 识别 + 模板系统
- `okki-email-sync` (task-002) - OKKI 双向同步

### 2. 配置检查

```bash
# 验证配置文件
node -e "JSON.parse(require('fs').readFileSync('config/follow-up-rules.json'))"
node -e "JSON.parse(require('fs').readFileSync('config/stage-transitions.json'))"
node -e "JSON.parse(require('fs').readFileSync('config/follow-up-strategies.json'))"
```

### 3. 运行定时调度器

```bash
# Dry-run 模式（预览将生成的草稿）
node scripts/follow-up-scheduler.js --dry-run

# 实际运行（生成草稿）
node scripts/follow-up-scheduler.js --mode auto

# 手动触发（立即执行）
node scripts/follow-up-scheduler.js --mode manual
```

### 4. 同步到 OKKI

```bash
# Dry-run 模式（预览将创建的 OKKI 记录）
node scripts/okki-integration.js --dry-run

# 实际同步
node scripts/okki-integration.js --sync
```

---

## ⚙️ 配置说明

### follow-up-rules.json

定义 6 个客户阶段和跟进规则：

| 阶段 ID | 名称 | 优先级 | 自动升级 |
|--------|------|--------|---------|
| new_inquiry | 新询盘 | high | 7 天无回复 → lost |
| quoted | 已报价 | high | 30 天无回复 → lost |
| sample_sent | 已样品 | medium | 14 天无反馈 → lost |
| negotiating | 谈判中 | high | 14 天停滞 → lost |
| closed_won | 成交 | low | - |
| lost | 流失 | low | - |

### follow-up-strategies.json

为每个阶段定义跟进序列：

```json
{
  "stage_id": "quoted",
  "follow_up_sequence": ["day_3", "day_7", "day_14", "day_30"],
  "templates": ["template-followup-quote-001", "..."],
  "escalation_rules": {...}
}
```

### global_settings

```json
{
  "working_hours": {
    "timezone": "Asia/Shanghai",
    "start_hour": 9,
    "end_hour": 18,
    "exclude_weekends": true
  },
  "max_follow_ups_per_stage": 3,
  "okki_sync_enabled": true,
  "require_wilson_approval_for_sending": true
}
```

---

## 📊 运行示例

### 场景 1：新询盘自动跟进

```bash
# 客户发送询盘邮件
# ↓ auto-capture.js 捕获邮件
# ↓ 客户阶段标记为 new_inquiry
# ↓ follow-up-scheduler.js 检测到 30 分钟无回复
# ↓ 生成首次回复草稿（template-inquiry-001）
# ↓ Discord 推送审阅通知
```

### 场景 2：报价后跟进序列

```bash
# 销售发送报价单
# ↓ 客户阶段更新为 quoted
# ↓ Day 3: 生成首次跟进草稿
# ↓ Day 7: 生成二次跟进草稿
# ↓ Day 14: 生成三次跟进草稿 + 升级提醒
# ↓ Day 30: 自动标记为 lost
```

### 场景 3：OKKI 同步

```bash
# follow-up-scheduler.js 生成草稿
# ↓ okki-integration.js 读取草稿
# ↓ 调用 OKKI API 创建跟进记录（trail_type=105）
# ↓ 更新草稿状态为 synced
# ↓ 记录同步日志
```

---

## 🔧 故障排查

### 问题：草稿未生成

```bash
# 检查配置文件
cat logs/scheduler-$(date +%Y-%m-%d).log

# 验证 OKKI 客户扫描
node scripts/follow-up-scheduler.js --dry-run --debug
```

### 问题：OKKI 同步失败

```bash
# 检查 OKKI CLI 连接
python3 /Users/wilson/.openclaw/workspace/xiaoman-okki/api/okki.py --help

# 查看同步日志
cat logs/okki-integration-$(date +%Y-%m-%d).log
```

### 问题：阶段流转不正确

```bash
# 检查阶段流转规则
cat config/stage-transitions.json | jq '.transition_rules[]'

# 手动触发阶段更新
node scripts/follow-up-scheduler.js --force-stage-update
```

---

## 📈 效果指标

| 指标 | 目标值 | 说明 |
|------|--------|------|
| 首次回复时间 | < 30 分钟 | 新询盘自动触发 |
| 跟进覆盖率 | 100% | 所有阶段客户 |
| OKKI 同步率 | > 95% | 草稿→跟进记录 |
| 人工审阅率 | 100% | 发送前必须确认 |

---

## 🛡️ 安全保证

- ✅ **严禁自动发送**：所有邮件发送必须经过 Discord 审阅确认
- ✅ **去重机制**：相同 UID 不重复创建跟进记录
- ✅ **工作时间限制**：仅在工作时间触发跟进（可配置）
- ✅ **日志完整**：所有操作记录到 logs/ 目录

---

## 📝 开发历史

- **2026-03-24**: Phase 2 首个 skill，6 次迭代完成
- **Subtask 1**: 跟进规则引擎配置（follow-up-rules.json）
- **Subtask 2**: 客户阶段流转模型（stage-transitions.json）
- **Subtask 3**: 跟进策略模板（follow-up-strategies.json）
- **Subtask 4**: 定时调度器（follow-up-scheduler.js）
- **Subtask 5**: OKKI 集成（okki-integration.js）

---

**Skill 版本**: 1.0  
**最后更新**: 2026-03-24  
**维护者**: Wilson Evolution System


path: config/follow-up-rules.json
content: {
  "version": "1.0",
  "created_at": "2026-03-24T13:53:00+08:00",
  "description": "Follow-up rule engine configuration for inquiry-to-order conversion automation",
  "stages": [
    {
      "id": "new_inquiry",
      "name_en": "New Inquiry",
      "name_cn": "新询盘",
      "description": "Customer has sent initial inquiry, no response yet",
      "priority": "high",
      "entry_trigger": "email_received:intent=inquiry"
    },
    {
      "id": "quoted",
      "name_en": "Quotation Sent",
      "name_cn": "已报价",
      "description": "Quotation has been sent to customer, awaiting response",
      "priority": "high",
      "entry_trigger": "email_sent:template=template-inquiry-001"
    },
    {
      "id": "sample_sent",
      "name_en": "Sample Sent",
      "name_cn": "已样品",
      "description": "Sample has been shipped to customer for evaluation",
      "priority": "medium",
      "entry_trigger": "okki_record:type=sample_shipped"
    },
    {
      "id": "negotiating",
      "name_en": "Negotiating",
      "name_cn": "谈判中",
      "description": "Customer is negotiating price, terms, or specifications",
      "priority": "high",
      "entry_trigger": "email_received:intent=inquiry+keywords=price|discount|terms"
    },
    {
      "id": "closed_won",
      "name_en": "Closed Won",
      "name_cn": "成交",
      "description": "Order confirmed and payment received",
      "priority": "low",
      "entry_trigger": "okki_record:type=order_confirmed"
    },
    {
      "id": "lost",
      "name_en": "Lost",
      "name_cn": "流失",
      "description": "Customer has not responded after multiple follow-ups or explicitly declined",
      "priority": "low",
      "entry_trigger": "follow_up:max_retries_exceeded OR email_received:intent=decline"
    }
  ],
  "rules": [
    {
      "id": "rule-new-inquiry-001",
      "stage": "new_inquiry",
      "name_en": "First Response",
      "name_cn": "首次回复",
      "trigger": {
        "type": "time_elapsed",
        "condition": "no_reply_sent",
        "delay_minutes": 30
      },
      "condition": {
        "type": "all",
        "checks": [
          { "field": "okki_follow_up_count", "operator": "equals", "value": 0 },
          { "field": "email_sent_count", "operator": "equals", "value": 0 }
        ]
      },
      "action": "generate_draft",
      "template": "template-inquiry-001",
      "max_retries": 1,
      "escalate_to": null
    },
    {
      "id": "rule-quoted-001",
      "stage": "quoted",
      "name_en": "First Follow-up After Quote",
      "name_cn": "报价后首次跟进",
      "trigger": {
        "type": "time_elapsed",
        "condition": "no_customer_reply",
        "delay_days": 3
      },
      "condition": {
        "type": "all",
        "checks": [
          { "field": "okki_follow_up_count", "operator": "greater_than", "value": 0 },
          { "field": "customer_reply_received", "operator": "equals", "value": false }
        ]
      },
      "action": "generate_draft",
      "template": "template-followup-quote-001",
      "max_retries": 1,
      "escalate_to": "rule-quoted-002"
    },
    {
      "id": "rule-quoted-002",
      "stage": "quoted",
      "name_en": "Second Follow-up After Quote",
      "name_cn": "报价后二次跟进",
      "trigger": {
        "type": "time_elapsed",
        "condition": "no_customer_reply",
        "delay_days": 7
      },
      "condition": {
        "type": "all",
        "checks": [
          { "field": "customer_reply_received", "operator": "equals", "value": false }
        ]
      },
      "action": "generate_draft",
      "template": "template-followup-quote-002",
      "max_retries": 1,
      "escalate_to": "rule-quoted-003"
    },
    {
      "id": "rule-quoted-003",
      "stage": "quoted",
      "name_en": "Third Follow-up After Quote",
      "name_cn": "报价后三次跟进",
      "trigger": {
        "type": "time_elapsed",
        "condition": "no_customer_reply",
        "delay_days": 14
      },
      "condition": {
        "type": "all",
        "checks": [
          { "field": "customer_reply_received", "operator": "equals", "value": false }
        ]
      },
      "action": "send_reminder",
      "template": "template-followup-quote-003",
      "max_retries": 1,
      "escalate_to": "lost"
    },
    {
      "id": "rule-sample-001",
      "stage": "sample_sent",
      "name_en": "Sample Tracking Follow-up",
      "name_cn": "样品寄送跟进",
      "trigger": {
        "type": "time_elapsed",
        "condition": "sample_delivered",
        "delay_days": 3
      },
      "condition": {
        "type": "all",
        "checks": [
          { "field": "tracking_status", "operator": "equals", "value": "delivered" }
        ]
      },
      "action": "generate_draft",
      "template": "template-followup-sample-001",
      "max_retries": 1,
      "escalate_to": "rule-sample-002"
    },
    {
      "id": "rule-sample-002",
      "stage": "sample_sent",
      "name_en": "Sample Feedback Request",
      "name_cn": "样品反馈请求",
      "trigger": {
        "type": "time_elapsed",
        "condition": "no_feedback_received",
        "delay_days": 7
      },
      "condition": {
        "type": "all",
        "checks": [
          { "field": "customer_feedback_received", "operator": "equals", "value": false }
        ]
      },
      "action": "generate_draft",
      "template": "template-followup-sample-002",
      "max_retries": 2,
      "escalate_to": "negotiating"
    },
    {
      "id": "rule-negotiating-001",
      "stage": "negotiating",
      "name_en": "Counter-offer Response",
      "name_cn": "还价回复",
      "trigger": {
        "type": "time_elapsed",
        "condition": "counter_offer_received",
        "delay_minutes": 60
      },
      "condition": {
        "type": "all",
        "checks": [
          { "field": "email_received:intent", "operator": "contains", "value": "price|discount" }
        ]
      },
      "action": "generate_draft",
      "template": "template-negotiation-001",
      "max_retries": 3,
      "escalate_to": null
    },
    {
      "id": "rule-negotiating-002",
      "stage": "negotiating",
      "name_en": "Deal Closing Push",
      "name_cn": "成交推动",
      "trigger": {
        "type": "time_elapsed",
        "condition": "negotiation_stalled",
        "delay_days": 5
      },
      "condition": {
        "type": "all",
        "checks": [
          { "field": "last_email_direction", "operator": "equals", "value": "outbound" },
          { "field": "customer_reply_received", "operator": "equals", "value": false }
        ]
      },
      "action": "generate_draft",
      "template": "template-negotiation-close-001",
      "max_retries": 2,
      "escalate_to": "lost"
    },
    {
      "id": "rule-won-001",
      "stage": "closed_won",
      "name_en": "Order Confirmation Thank You",
      "name_cn": "订单确认感谢",
      "trigger": {
        "type": "event",
        "condition": "order_confirmed",
        "delay_minutes": 5
      },
      "condition": null,
      "action": "generate_draft",
      "template": "template-thankyou-001",
      "max_retries": 1,
      "escalate_to": null
    },
    {
      "id": "rule-lost-001",
      "stage": "lost",
      "name_en": "Lost Customer Archive",
      "name_cn": "流失客户归档",
      "trigger": {
        "type": "event",
        "condition": "stage_changed_to_lost",
        "delay_minutes": 0
      },
      "condition": null,
      "action": "archive_and_notify",
      "template": null,
      "max_retries": 1,
      "escalate_to": null
    }
  ],
  "templates": {
    "template-inquiry-001": {
      "source": "email-smart-reply",
      "intent": "inquiry",
      "subject_line": "Re: Your inquiry about {{product_category}}",
      "variables": ["product_category", "customer_name", "company_name"]
    },
    "template-followup-quote-001": {
      "source": "follow-up-engine",
      "subject_line": "Following up on your quotation request",
      "variables": ["quotation_ref", "product_name", "validity_date"]
    },
    "template-followup-quote-002": {
      "source": "follow-up-engine",
      "subject_line": "Quick check: any questions about the quote?",
      "variables": ["quotation_ref", "product_name"]
    },
    "template-followup-quote-003": {
      "source": "follow-up-engine",
      "subject_line": "Last reminder: quotation expires soon",
      "variables": ["quotation_ref", "expiry_date"]
    },
    "template-followup-sample-001": {
      "source": "follow-up-engine",
      "subject_line": "Your sample has been delivered",
      "variables": ["tracking_number", "product_name"]
    },
    "template-followup-sample-002": {
      "source": "follow-up-engine",
      "subject_line": "How did the sample work for you?",
      "variables": ["product_name", "customer_name"]
    },
    "template-negotiation-001": {
      "source": "follow-up-engine",
      "subject_line": "Re: Your counter-offer",
      "variables": ["original_price", "counter_price", "our_offer"]
    },
    "template-negotiation-close-001": {
      "source": "follow-up-engine",
      "subject_line": "Special offer to close this deal",
      "variables": ["special_price", "deadline"]
    },
    "template-thankyou-001": {
      "source": "email-smart-reply",
      "intent": "partnership",
      "subject_line": "Thank you for your order!",
      "variables": ["order_number", "customer_name"]
    }
  },
  "global_settings": {
    "default_retry_interval_days": 3,
    "max_follow_ups_per_stage": 3,
    "business_hours_only": true,
    "business_hours": {
      "timezone": "Asia/Shanghai",
      "start_hour": 9,
      "end_hour": 18,
      "exclude_weekends": true
    },
    "notification_channels": ["discord:inquiry-followup"],
    "okki_sync_enabled": true,
    "auto_draft_enabled": true,
    "require_wilson_approval_for_sending": true
  }
}


path: config/follow-up-strategies.json
content: {
  "strategies": [
    {
      "stage_id": "new_inquiry",
      "stage_name": "新询盘",
      "follow_up_sequence": [1, 3, 7, 14, 30],
      "templates": [
        {"day": 1, "template_id": "template-inquiry-001", "intent": "initial_response"},
        {"day": 3, "template_id": "template-followup-001", "intent": "price_followup"},
        {"day": 7, "template_id": "template-followup-002", "intent": "value_proposition"},
        {"day": 14, "template_id": "template-followup-003", "intent": "case_study"},
        {"day": 30, "template_id": "template-followup-004", "intent": "final_check"}
      ],
      "escalation_rules": {
        "no_reply_after_followups": 3,
        "action": "mark_as_cold",
        "notify_owner": true,
        "suggested_action": "尝试电话或 WhatsApp 联系"
      },
      "channel": "email",
      "working_hours_only": true
    },
    {
      "stage_id": "quoted",
      "stage_name": "已报价",
      "follow_up_sequence": [2, 5, 10, 20],
      "templates": [
        {"day": 2, "template_id": "template-quote-001", "intent": "quote_confirmation"},
        {"day": 5, "template_id": "template-quote-002", "intent": "price_negotiation"},
        {"day": 10, "template_id": "template-quote-003", "intent": "urgency_create"},
        {"day": 20, "template_id": "template-quote-004", "intent": "last_chance"}
      ],
      "escalation_rules": {
        "no_reply_after_followups": 2,
        "action": "offer_discount",
        "notify_owner": true,
        "suggested_action": "申请特价审批或提供样品"
      },
      "channel": "email",
      "working_hours_only": true
    },
    {
      "stage_id": "sample_sent",
      "stage_name": "已寄样",
      "follow_up_sequence": [1, 3, 7, 14],
      "templates": [
        {"day": 1, "template_id": "template-sample-001", "intent": "tracking_notify"},
        {"day": 3, "template_id": "template-sample-002", "intent": "delivery_confirm"},
        {"day": 7, "template_id": "template-sample-003", "intent": "feedback_request"},
        {"day": 14, "template_id": "template-sample-004", "intent": "test_result_followup"}
      ],
      "escalation_rules": {
        "no_reply_after_followups": 2,
        "action": "schedule_call",
        "notify_owner": true,
        "suggested_action": "安排视频会议讨论样品测试结果"
      },
      "channel": "email",
      "working_hours_only": true
    },
    {
      "stage_id": "negotiating",
      "stage_name": "谈判中",
      "follow_up_sequence": [1, 2, 5, 10],
      "templates": [
        {"day": 1, "template_id": "template-nego-001", "intent": "counter_offer_response"},
        {"day": 2, "template_id": "template-nego-002", "intent": "terms_clarification"},
        {"day": 5, "template_id": "template-nego-003", "intent": "deadline_reminder"},
        {"day": 10, "template_id": "template-nego-004", "intent": "final_offer"}
      ],
      "escalation_rules": {
        "no_reply_after_followups": 2,
        "action": "escalate_to_manager",
        "notify_owner": true,
        "suggested_action": "升级至销售经理介入谈判"
      },
      "channel": "email",
      "working_hours_only": true
    },
    {
      "stage_id": "closed_won",
      "stage_name": "已成交",
      "follow_up_sequence": [1, 7, 30, 90],
      "templates": [
        {"day": 1, "template_id": "template-won-001", "intent": "order_confirmation"},
        {"day": 7, "template_id": "template-won-002", "intent": "production_update"},
        {"day": 30, "template_id": "template-won-003", "intent": "delivery_notify"},
        {"day": 90, "template_id": "template-won-004", "intent": "satisfaction_survey"}
      ],
      "escalation_rules": {
        "no_reply_after_followups": 1,
        "action": "account_review",
        "notify_owner": false,
        "suggested_action": "标记为低活跃客户，季度回顾"
      },
      "channel": "email",
      "working_hours_only": false
    },
    {
      "stage_id": "lost",
      "stage_name": "已流失",
      "follow_up_sequence": [30, 90, 180],
      "templates": [
        {"day": 30, "template_id": "template-lost-001", "intent": "winback_offer"},
        {"day": 90, "template_id": "template-lost-002", "intent": "new_product_notify"},
        {"day": 180, "template_id": "template-lost-003", "intent": "seasonal_promotion"}
      ],
      "escalation_rules": {
        "no_reply_after_followups": 3,
        "action": "archive",
        "notify_owner": false,
        "suggested_action": "归档至历史客户库，年度激活尝试"
      },
      "channel": "email",
      "working_hours_only": false
    }
  ],
  "templates_reference": [
    {
      "template_id": "template-inquiry-001",
      "subject": "Re: Your inquiry about {product_name} - {company_name}",
      "body_key": "inquiry_initial_response",
      "intent": "initial_response",
      "tone": "professional"
    },
    {
      "template_id": "template-followup-001",
      "subject": "Following up on your {product_name} inquiry",
      "body_key": "price_followup",
      "intent": "price_followup",
      "tone": "friendly"
    },
    {
      "template_id": "template-followup-002",
      "subject": "Why {company_name} is the right partner for {product_name}",
      "body_key": "value_proposition",
      "intent": "value_proposition",
      "tone": "professional"
    },
    {
      "template_id": "template-followup-003",
      "subject": "Case study: How we helped {similar_company} with {product_name}",
      "body_key": "case_study_share",
      "intent": "case_study",
      "tone": "friendly"
    },
    {
      "template_id": "template-followup-004",
      "subject": "Still interested in {product_name}?",
      "body_key": "final_interest_check",
      "intent": "final_check",
      "tone": "friendly"
    },
    {
      "template_id": "template-quote-001",
      "subject": "Quotation #{quote_number} for {product_name} - Confirmation",
      "body_key": "quote_confirmation",
      "intent": "quote_confirmation",
      "tone": "professional"
    },
    {
      "template_id": "template-quote-002",
      "subject": "Flexible pricing options for {product_name}",
      "body_key": "price_negotiation",
      "intent": "price_negotiation",
      "tone": "friendly"
    },
    {
      "template_id": "template-quote-003",
      "subject": "Limited stock alert: {product_name}",
      "body_key": "urgency_create",
      "intent": "urgency_create",
      "tone": "urgent"
    },
    {
      "template_id": "template-quote-004",
      "subject": "Final chance: Quote #{quote_number} expires soon",
      "body_key": "last_chance",
      "intent": "last_chance",
      "tone": "urgent"
    },
    {
      "template_id": "template-sample-001",
      "subject": "Sample shipped: Tracking #{tracking_number}",
      "body_key": "tracking_notify",
      "intent": "tracking_notify",
      "tone": "professional"
    },
    {
      "template_id": "template-sample-002",
      "subject": "Did you receive the {product_name} sample?",
      "body_key": "delivery_confirm",
      "intent": "delivery_confirm",
      "tone": "friendly"
    },
    {
      "template_id": "template-sample-003",
      "subject": "Your feedback on {product_name} sample",
      "body_key": "feedback_request",
      "intent": "feedback_request",
      "tone": "friendly"
    },
    {
      "template_id": "template-sample-004",
      "subject": "Sample test results discussion",
      "body_key": "test_result_followup",
      "intent": "test_result_followup",
      "tone": "professional"
    },
    {
      "template_id": "template-nego-001",
      "subject": "Re: Your counter offer for {product_name}",
      "body_key": "counter_offer_response",
      "intent": "counter_offer_response",
      "tone": "professional"
    },
    {
      "template_id": "template-nego-002",
      "subject": "Clarification on terms for order #{order_number}",
      "body_key": "terms_clarification",
      "intent": "terms_clarification",
      "tone": "professional"
    },
    {
      "template_id": "template-nego-003",
      "subject": "Production slot deadline: {deadline_date}",
      "body_key": "deadline_reminder",
      "intent": "deadline_reminder",
      "tone": "urgent"
    },
    {
      "template_id": "template-nego-004",
      "subject": "Final offer for {product_name} - Valid until {expiry_date}",
      "body_key": "final_offer",
      "intent": "final_offer",
      "tone": "professional"
    },
    {
      "template_id": "template-won-001",
      "subject": "Order #{order_number} confirmed - Thank you!",
      "body_key": "order_confirmation",
      "intent": "order_confirmation",
      "tone": "friendly"
    },
    {
      "template_id": "template-won-002",
      "subject": "Production update: Order #{order_number}",
      "body_key": "production_update",
      "intent": "production_update",
      "tone": "professional"
    },
    {
      "template_id": "template-won-003",
      "subject": "Your order #{order_number} has been shipped",
      "body_key": "delivery_notify",
      "intent": "delivery_notify",
      "tone": "friendly"
    },
    {
      "template_id": "template-won-004",
      "subject": "How satisfied are you with {product_name}?",
      "body_key": "satisfaction_survey",
      "intent": "satisfaction_survey",
      "tone": "friendly"
    },
    {
      "template_id": "template-lost-001",
      "subject": "Special win-back offer for {customer_name}",
      "body_key": "winback_offer",
      "intent": "winback_offer",
      "tone": "friendly"
    },
    {
      "template_id": "template-lost-002",
      "subject": "New product launch: {new_product_name}",
      "body_key": "new_product_notify",
      "intent": "new_product_notify",
      "tone": "friendly"
    },
    {
      "template_id": "template-lost-003",
      "subject": "Seasonal promotion: {product_name}",
      "body_key": "seasonal_promotion",
      "intent": "seasonal_promotion",
      "tone": "friendly"
    }
  ],
  "global_settings": {
    "working_hours": {
      "timezone": "Asia/Shanghai",
      "start": "09:00",
      "end": "18:00",
      "exclude_weekends": true,
      "exclude_holidays": true
    },
    "max_follow_ups": 5,
    "default_interval_days": 3,
    "auto_pause_on_reply": true,
    "okki_sync": {
      "enabled": true,
      "sync_followup_records": true,
      "update_stage_on_reply": true
    },
    "notification": {
      "discord_channel": "follow-up-alerts",
      "notify_on_escalation": true,
      "notify_on_stage_change": true
    }
  }
}


path: config/stage-transitions.json
content: {
  "version": "1.0",
  "created_at": "2026-03-24T14:03:00+08:00",
  "description": "Customer stage state transition model for inquiry-to-order conversion automation",
  "stages": [
    {
      "id": "new_inquiry",
      "name_en": "New Inquiry",
      "name_cn": "新询盘",
      "entry_conditions": [
        { "type": "event", "source": "email", "condition": "intent=inquiry" },
        { "type": "event", "source": "okki", "condition": "lead_created" }
      ],
      "exit_conditions": [
        { "type": "action", "condition": "email_sent:template=inquiry_response" },
        { "type": "time", "condition": "no_response_7_days" }
      ],
      "auto_transitions": [
        { "to_stage": "lost", "trigger": "no_activity", "delay_days": 7 }
      ],
      "manual_transitions": [
        { "to_stage": "quoted", "reason": "quotation_sent" },
        { "to_stage": "lost", "reason": "invalid_inquiry" }
      ]
    },
    {
      "id": "quoted",
      "name_en": "Quotation Sent",
      "name_cn": "已报价",
      "entry_conditions": [
        { "type": "action", "source": "email", "condition": "template=template-inquiry-001" },
        { "type": "record", "source": "okki", "condition": "quotation_recorded" }
      ],
      "exit_conditions": [
        { "type": "event", "condition": "customer_reply_received" },
        { "type": "event", "condition": "sample_requested" },
        { "type": "time", "condition": "no_response_30_days" }
      ],
      "auto_transitions": [
        { "to_stage": "sample_sent", "trigger": "sample_request_detected" },
        { "to_stage": "negotiating", "trigger": "counter_offer_received" },
        { "to_stage": "lost", "trigger": "no_activity", "delay_days": 30 }
      ],
      "manual_transitions": [
        { "to_stage": "sample_sent", "reason": "sample_shipped" },
        { "to_stage": "negotiating", "reason": "price_negotiation_started" },
        { "to_stage": "closed_won", "reason": "order_confirmed" },
        { "to_stage": "lost", "reason": "customer_declined" }
      ]
    },
    {
      "id": "sample_sent",
      "name_en": "Sample Sent",
      "name_cn": "已样品",
      "entry_conditions": [
        { "type": "record", "source": "okki", "condition": "sample_shipped" },
        { "type": "event", "source": "logistics", "condition": "tracking_created" }
      ],
      "exit_conditions": [
        { "type": "event", "condition": "sample_delivered" },
        { "type": "event", "condition": "feedback_received" },
        { "type": "time", "condition": "no_feedback_14_days" }
      ],
      "auto_transitions": [
        { "to_stage": "negotiating", "trigger": "positive_feedback" },
        { "to_stage": "lost", "trigger": "no_activity", "delay_days": 14 }
      ],
      "manual_transitions": [
        { "to_stage": "negotiating", "reason": "customer_interested" },
        { "to_stage": "quoted", "reason": "resend_quote_after_sample" },
        { "to_stage": "lost", "reason": "sample_rejected" }
      ]
    },
    {
      "id": "negotiating",
      "name_en": "Negotiating",
      "name_cn": "谈判中",
      "entry_conditions": [
        { "type": "event", "source": "email", "condition": "intent=price_discussion" },
        { "type": "event", "source": "email", "condition": "keywords=discount|terms|MOQ" }
      ],
      "exit_conditions": [
        { "type": "event", "condition": "order_confirmed" },
        { "type": "event", "condition": "customer_declined" },
        { "type": "time", "condition": "negotiation_stalled_14_days" }
      ],
      "auto_transitions": [
        { "to_stage": "closed_won", "trigger": "payment_received" },
        { "to_stage": "lost", "trigger": "no_activity", "delay_days": 14 }
      ],
      "manual_transitions": [
        { "to_stage": "closed_won", "reason": "deal_closed" },
        { "to_stage": "quoted", "reason": "revision_requested" },
        { "to_stage": "lost", "reason": "negotiation_failed" }
      ]
    },
    {
      "id": "closed_won",
      "name_en": "Closed Won",
      "name_cn": "成交",
      "entry_conditions": [
        { "type": "record", "source": "okki", "condition": "order_confirmed" },
        { "type": "event", "source": "finance", "condition": "payment_received" }
      ],
      "exit_conditions": [
        { "type": "event", "condition": "order_completed" },
        { "type": "event", "condition": "repeat_order" }
      ],
      "auto_transitions": [],
      "manual_transitions": [
        { "to_stage": "new_inquiry", "reason": "repeat_order" },
        { "to_stage": "lost", "reason": "order_cancelled" }
      ]
    },
    {
      "id": "lost",
      "name_en": "Lost",
      "name_cn": "流失",
      "entry_conditions": [
        { "type": "event", "condition": "max_followups_exceeded" },
        { "type": "event", "condition": "explicit_decline" },
        { "type": "time", "condition": "no_activity_threshold" }
      ],
      "exit_conditions": [
        { "type": "event", "condition": "reactivation_request" }
      ],
      "auto_transitions": [],
      "manual_transitions": [
        { "to_stage": "new_inquiry", "reason": "customer_reactivated" }
      ]
    }
  ],
  "transition_rules": [
    {
      "id": "trans-001",
      "from_stage": "new_inquiry",
      "to_stage": "quoted",
      "trigger_event": "email_sent",
      "conditions": [
        { "field": "email.template", "operator": "equals", "value": "template-inquiry-001" }
      ],
      "auto": false,
      "description": "Initial quotation sent to customer"
    },
    {
      "id": "trans-002",
      "from_stage": "new_inquiry",
      "to_stage": "lost",
      "trigger_event": "time_elapsed",
      "conditions": [
        { "field": "days_since_last_activity", "operator": "greater_than", "value": 7 }
      ],
      "auto": true,
      "description": "No response after 7 days, mark as lost"
    },
    {
      "id": "trans-003",
      "from_stage": "quoted",
      "to_stage": "sample_sent",
      "trigger_event": "okki_record_created",
      "conditions": [
        { "field": "record.type", "operator": "equals", "value": "sample_shipped" }
      ],
      "auto": true,
      "description": "Sample shipped, transition to sample_sent stage"
    },
    {
      "id": "trans-004",
      "from_stage": "quoted",
      "to_stage": "negotiating",
      "trigger_event": "email_received",
      "conditions": [
        { "field": "email.intent", "operator": "contains", "value": "price|discount|terms" }
      ],
      "auto": true,
      "description": "Customer counter-offer detected"
    },
    {
      "id": "trans-005",
      "from_stage": "quoted",
      "to_stage": "closed_won",
      "trigger_event": "okki_record_created",
      "conditions": [
        { "field": "record.type", "operator": "equals", "value": "order_confirmed" }
      ],
      "auto": false,
      "description": "Order confirmed without sample stage"
    },
    {
      "id": "trans-006",
      "from_stage": "quoted",
      "to_stage": "lost",
      "trigger_event": "time_elapsed",
      "conditions": [
        { "field": "days_since_quote_sent", "operator": "greater_than", "value": 30 }
      ],
      "auto": true,
      "description": "No response 30 days after quote, mark as lost"
    },
    {
      "id": "trans-007",
      "from_stage": "sample_sent",
      "to_stage": "negotiating",
      "trigger_event": "email_received",
      "conditions": [
        { "field": "email.sentiment", "operator": "equals", "value": "positive" }
      ],
      "auto": true,
      "description": "Positive sample feedback, move to negotiation"
    },
    {
      "id": "trans-008",
      "from_stage": "sample_sent",
      "to_stage": "lost",
      "trigger_event": "time_elapsed",
      "conditions": [
        { "field": "days_since_sample_delivered", "operator": "greater_than", "value": 14 }
      ],
      "auto": true,
      "description": "No feedback 14 days after delivery, mark as lost"
    },
    {
      "id": "trans-009",
      "from_stage": "negotiating",
      "to_stage": "closed_won",
      "trigger_event": "okki_record_created",
      "conditions": [
        { "field": "record.type", "operator": "equals", "value": "payment_received" }
      ],
      "auto": true,
      "description": "Payment received, deal closed"
    },
    {
      "id": "trans-010",
      "from_stage": "negotiating",
      "to_stage": "lost",
      "trigger_event": "time_elapsed",
      "conditions": [
        { "field": "days_since_last_negotiation", "operator": "greater_than", "value": 14 }
      ],
      "auto": true,
      "description": "Negotiation stalled 14 days, mark as lost"
    },
    {
      "id": "trans-011",
      "from_stage": "closed_won",
      "to_stage": "new_inquiry",
      "trigger_event": "email_received",
      "conditions": [
        { "field": "email.intent", "operator": "equals", "value": "repeat_order" }
      ],
      "auto": false,
      "description": "Repeat order from existing customer"
    },
    {
      "id": "trans-012",
      "from_stage": "lost",
      "to_stage": "new_inquiry",
      "trigger_event": "email_received",
      "conditions": [
        { "field": "email.intent", "operator": "equals", "value": "reactivation" }
      ],
      "auto": false,
      "description": "Lost customer reactivates"
    }
  ],
  "fallback_rules": [
    {
      "id": "fallback-001",
      "name_en": "Unknown Stage Default",
      "name_cn": "未知阶段默认处理",
      "condition": "stage_not_recognized",
      "action": "assign_to_new_inquiry",
      "notify": "discord:inquiry-followup",
      "description": "If customer stage cannot be determined, default to new_inquiry and notify"
    },
    {
      "id": "fallback-002",
      "name_en": "Conflicting Transitions",
      "name_cn": "冲突流转处理",
      "condition": "multiple_transitions_triggered",
      "action": "priority_based_resolution",
      "priority_order": ["closed_won", "negotiating", "sample_sent", "quoted", "new_inquiry", "lost"],
      "notify": "discord:inquiry-followup",
      "description": "If multiple transitions are triggered simultaneously, use priority order to resolve"
    },
    {
      "id": "fallback-003",
      "name_en": "Orphaned Customer Recovery",
      "name_cn": "孤立客户恢复",
      "condition": "no_activity_60_days",
      "action": "review_and_archive",
      "notify": "wilson",
      "description": "Customers with no activity for 60 days require manual review before archiving"
    },
    {
      "id": "fallback-004",
      "name_en": "Invalid Transition Attempt",
      "name_cn": "无效流转尝试处理",
      "condition": "transition_not_in_rules",
      "action": "reject_and_log",
      "notify": "discord:inquiry-followup",
      "description": "If a transition is attempted that is not defined in transition_rules, reject and log for review"
    }
  ]
}


path: scripts/follow-up-scheduler.js
content: #!/usr/bin/env node

/**
 * Follow-up Scheduler - 自动跟进调度器
 * 
 * 功能：
 * 1. 读取 follow-up-strategies.json 获取各阶段的跟进序列
 * 2. 扫描 OKKI 客户数据，识别每个客户当前所处阶段
 * 3. 计算每个客户的下次跟进时间（基于阶段策略的 follow_up_sequence）
 * 4. 对到期需要跟进的客户，自动生成跟进邮件草稿
 * 5. 将草稿保存到指定目录，或创建 OKKI 跟进记录（draft 状态）
 * 6. 支持 dry-run 模式（预览将生成的草稿）
 * 7. 支持手动触发和定时触发两种模式
 */

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

// 配置路径
const CONFIG_DIR = path.join(__dirname, '..', 'config');
const STRATEGIES_FILE = path.join(CONFIG_DIR, 'follow-up-strategies.json');
const DRAFTS_DIR = path.join(__dirname, '..', 'drafts');
const LOGS_DIR = path.join(__dirname, '..', 'logs');

// 确保目录存在
[DRAFTS_DIR, LOGS_DIR].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(LOGS_DIR, `scheduler-${new Date().toISOString().split('T')[0]}.log`);
  fs.appendFileSync(logFile, logLine + '\n');
}

// 加载配置文件
function loadStrategies() {
  if (!fs.existsSync(STRATEGIES_FILE)) {
    throw new Error(`配置文件不存在：${STRATEGIES_FILE}`);
  }
  const content = fs.readFileSync(STRATEGIES_FILE, 'utf8');
  return JSON.parse(content);
}

// 检查工作时间内
function isWorkingHours(settings) {
  const now = new Date();
  const tz = settings.timezone || 'Asia/Shanghai';
  
  // 简单实现：使用系统时间（实际应使用时区转换）
  const hours = now.getHours();
  const minutes = now.getMinutes();
  const dayOfWeek = now.getDay(); // 0 = Sunday, 6 = Saturday
  
  const [startHour, startMin] = settings.start.split(':').map(Number);
  const [endHour, endMin] = settings.end.split(':').map(Number);
  
  const currentTime = hours * 60 + minutes;
  const startTime = startHour * 60 + startMin;
  const endTime = endHour * 60 + endMin;
  
  if (settings.exclude_weekends && (dayOfWeek === 0 || dayOfWeek === 6)) {
    return false;
  }
  
  return currentTime >= startTime && currentTime <= endTime;
}

// 计算下次跟进日期
function calculateNextFollowUpDate(lastContactDate, followUpSequence, followUpIndex) {
  if (!lastContactDate || followUpIndex >= followUpSequence.length) {
    return null;
  }
  
  const daysToAdd = followUpSequence[followUpIndex];
  const nextDate = new Date(lastContactDate);
  nextDate.setDate(nextDate.getDate() + daysToAdd);
  
  return nextDate;
}

// 获取客户当前跟进索引
function getFollowUpIndex(customer, strategy) {
  // 从客户数据中获取已完成的跟进次数
  // 实际实现需要从 OKKI 或本地记录中读取
  return customer.follow_up_count || 0;
}

// 模拟 OKKI 客户数据扫描（实际实现需调用 OKKI API）
function scanOKKICustomers() {
  log('扫描 OKKI 客户数据...');
  
  // 模拟数据 - 实际实现应调用 OKKI API
  // 参考 task-002 的 OKKI 同步接口
  const mockCustomers = [
    {
      id: 'okki_001',
      name: 'ABC Trading Ltd',
      email: '[REDACTED_SECRET]',
      stage: 'new_inquiry',
      last_contact_date: '2026-03-20',
      follow_up_count: 0,
      owner: 'wilson',
      product_interest: 'HDMI Cable'
    },
    {
      id: 'okki_002',
      name: 'XYZ Electronics',
      email: '[REDACTED_SECRET]',
      stage: 'quoted',
      last_contact_date: '2026-03-18',
      follow_up_count: 1,
      owner: 'wilson',
      quote_number: 'QT-2026-0042',
      product_interest: 'DP Cable'
    },
    {
      id: 'okki_003',
      name: 'Global Tech Inc',
      email: '[REDACTED_SECRET]',
      stage: 'sample_sent',
      last_contact_date: '2026-03-22',
      follow_up_count: 0,
      owner: 'wilson',
      tracking_number: 'SF1234567890',
      product_interest: 'USB-C Cable'
    }
  ];
  
  log(`找到 ${mockCustomers.length} 个客户`);
  return mockCustomers;
}

// 生成邮件草稿
function generateEmailDraft(customer, template, strategy) {
  const now = new Date();
  const draftId = `draft-${now.toISOString().replace(/[:.]/g, '-')}-${customer.id}`;
  
  // 替换模板变量（安全替换，处理未定义变量）
  let subject = template.subject || '';
  const replacements = {
    '{product_name}': customer.product_interest || 'our products',
    '{company_name}': customer.name,
    '{quote_number}': customer.quote_number || '',
    '{tracking_number}': customer.tracking_number || '',
    '{customer_name}': customer.name,
    '{order_number}': customer.order_number || '',
    '{deadline_date}': '',
    '{expiry_date}': '',
    '{similar_company}': '',
    '{new_product_name}': '',
    '{seasonal_promotion}': ''
  };
  
  for (const [placeholder, value] of Object.entries(replacements)) {
    subject = subject.split(placeholder).join(value);
  }
  
  const draft = {
    draft_id: draftId,
    customer_id: customer.id,
    customer_name: customer.name,
    customer_email: customer.email,
    stage: customer.stage,
    template_id: template.template_id,
    intent: template.intent,
    subject: subject,
    body_key: template.body_key,
    tone: template.tone,
    follow_up_day: strategy.follow_up_sequence[customer.follow_up_count || 0],
    created_at: now.toISOString(),
    status: 'draft',
    owner: customer.owner
  };
  
  return draft;
}

// 保存草稿到文件
function saveDraft(draft) {
  const filename = `${draft.draft_id}.json`;
  const filepath = path.join(DRAFTS_DIR, filename);
  
  fs.writeFileSync(filepath, JSON.stringify(draft, null, 2), 'utf8');
  log(`草稿已保存：${filename}`);
  
  return filepath;
}

// 创建 OKKI 跟进记录（draft 状态）
function createOKKIFollowUpRecord(draft) {
  log(`[OKKI] 创建跟进记录（draft）：${draft.customer_id}`);
  
  // 实际实现应调用 OKKI API
  // 参考 task-002 的 OKKI 同步接口
  const record = {
    okki_record_id: `okki_fu_${Date.now()}`,
    customer_id: draft.customer_id,
    type: 'email_draft',
    status: 'draft',
    content: {
      subject: draft.subject,
      body_key: draft.body_key,
      template_id: draft.template_id
    },
    scheduled_date: new Date().toISOString(),
    owner: draft.owner
  };
  
  // 保存到本地（模拟 OKKI 记录）
  const recordFile = path.join(DRAFTS_DIR, `okki-${record.okki_record_id}.json`);
  fs.writeFileSync(recordFile, JSON.stringify(record, null, 2), 'utf8');
  
  return record;
}

// 主调度逻辑
function runScheduler(options = {}) {
  const { dryRun = false, manual = true } = options;
  
  log('='.repeat(60));
  log('Follow-up Scheduler 启动');
  log(`模式：${dryRun ? 'DRY-RUN' : 'LIVE'} | 触发：${manual ? '手动' : '定时'}`);
  log('='.repeat(60));
  
  try {
    // 1. 加载配置
    const config = loadStrategies();
    log(`加载 ${config.strategies.length} 个跟进策略`);
    
    const globalSettings = config.global_settings;
    
    // 2. 检查工作时间（如果配置了）
    if (globalSettings.working_hours.working_hours_only && !dryRun) {
      if (!isWorkingHours(globalSettings.working_hours)) {
        log('当前不在工作时间内，跳过执行', 'WARN');
        return { skipped: true, reason: 'outside_working_hours' };
      }
    }
    
    // 3. 扫描 OKKI 客户
    const customers = scanOKKICustomers();
    
    // 4. 为每个客户计算跟进计划
    const draftsToGenerate = [];
    
    for (const customer of customers) {
      // 查找对应阶段的策略
      const strategy = config.strategies.find(s => s.stage_id === customer.stage);
      
      if (!strategy) {
        log(`客户 ${customer.name} 阶段 ${customer.stage} 无对应策略，跳过`, 'WARN');
        continue;
      }
      
      // 获取当前跟进索引
      const followUpIndex = getFollowUpIndex(customer, strategy);
      
      // 检查是否已达到最大跟进次数
      if (followUpIndex >= strategy.follow_up_sequence.length) {
        log(`客户 ${customer.name} 已达到最大跟进次数 (${followUpIndex})，跳过`);
        continue;
      }
      
      // 计算下次跟进日期
      const lastContactDate = new Date(customer.last_contact_date);
      const nextFollowUpDate = calculateNextFollowUpDate(
        lastContactDate,
        strategy.follow_up_sequence,
        followUpIndex
      );
      
      if (!nextFollowUpDate) {
        log(`客户 ${customer.name} 无法计算下次跟进日期，跳过`, 'WARN');
        continue;
      }
      
      // 检查是否到期
      const now = new Date();
      const isDue = nextFollowUpDate <= now;
      
      if (!isDue) {
        log(`客户 ${customer.name} 下次跟进日期：${nextFollowUpDate.toISOString().split('T')[0]}，未到期`);
        continue;
      }
      
      // 获取对应模板
      const template = strategy.templates.find(t => {
        const day = strategy.follow_up_sequence[followUpIndex];
        return t.day === day;
      });
      
      if (!template) {
        log(`客户 ${customer.name} 未找到第 ${followUpIndex + 1} 次跟进模板，跳过`, 'WARN');
        continue;
      }
      
      log(`✓ 客户 ${customer.name} 到期需要跟进（阶段：${customer.stage}，第 ${followUpIndex + 1} 次）`);
      draftsToGenerate.push({ customer, template, strategy });
    }
    
    // 5. 生成草稿
    log('-'.repeat(60));
    log(`将生成 ${draftsToGenerate.length} 个跟进草稿`);
    log('-'.repeat(60));
    
    const results = {
      total_customers: customers.length,
      drafts_generated: 0,
      drafts: []
    };
    
    for (const { customer, template, strategy } of draftsToGenerate) {
      const draft = generateEmailDraft(customer, template, strategy);
      
      if (dryRun) {
        log(`[DRY-RUN] 将生成草稿：${draft.draft_id}`);
        log(`  客户：${customer.name} (${customer.email})`);
        log(`  主题：${draft.subject}`);
        log(`  模板：${template.template_id}`);
      } else {
        // 保存草稿文件
        saveDraft(draft);
        
        // 创建 OKKI 跟进记录
        if (globalSettings.okki_sync.enabled) {
          createOKKIFollowUpRecord(draft);
        }
        
        results.drafts_generated++;
        results.drafts.push(draft);
      }
    }
    
    // 6. 输出摘要
    log('='.repeat(60));
    log('执行完成摘要');
    log('='.repeat(60));
    log(`扫描客户总数：${results.total_customers}`);
    log(`生成草稿数量：${results.drafts_generated}`);
    
    if (dryRun) {
      log('');
      log('DRY-RUN 模式：未实际生成草稿');
    }
    
    return {
      success: true,
      ...results,
      dry_run: dryRun
    };
    
  } catch (error) {
    log(`执行失败：${error.message}`, 'ERROR');
    return {
      success: false,
      error: error.message
    };
  }
}

// CLI 入口
function main() {
  const args = process.argv.slice(2);
  
  const options = {
    dryRun: args.includes('--dry-run') || args.includes('-n'),
    manual: !args.includes('--scheduled')
  };
  
  const result = runScheduler(options);
  
  // 输出 JSON 结果（便于 cron 调用）
  console.log('\n--- JSON RESULT ---');
  console.log(JSON.stringify(result, null, 2));
  
  // 退出码
  process.exit(result.success ? 0 : 1);
}

// 导出供其他模块使用
module.exports = {
  runScheduler,
  loadStrategies,
  scanOKKICustomers,
  generateEmailDraft,
  saveDraft,
  createOKKIFollowUpRecord
};

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


path: scripts/okki-integration.js
content: #!/usr/bin/env node

/**
 * OKKI Integration - 跟进草稿同步到 OKKI 系统
 * 
 * 功能：
 * 1. 读取 follow-up-scheduler.js 生成的草稿文件
 * 2. 调用 OKKI API 创建跟进记录（trail_type=105 社交平台/通用跟进）
 * 3. 支持批量同步（多个客户的跟进记录）
 * 4. 支持 dry-run 模式（预览将创建的 OKKI 记录）
 * 5. 同步后更新草稿状态为 synced
 * 6. 记录同步日志（成功/失败）
 */

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

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

const CONFIG = {
  // OKKI CLI 路径
  okkiCliPath: process.env.OKKI_CLI_PATH || path.join(os.homedir(), '.openclaw', 'workspace', 'xiaoman-okki', 'api', 'okki.py'),
  
  // 草稿目录
  draftsDir: path.join(__dirname, '..', 'drafts'),
  
  // 日志目录
  logsDir: path.join(__dirname, '..', 'logs'),
  
  // 同步记录文件（去重用）
  syncRecordFile: process.env.OKKI_SYNC_RECORD_FILE || path.join(os.tmpdir(), 'follow-up-okki-sync-processed.json'),
  
  // Trail 类型
  TRAIL_TYPE: {
    FOLLOW_UP: 105  // 社交平台/通用跟进
  }
};

// 确保目录存在
[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, `okki-integration-${new Date().toISOString().split('T')[0]}.log`);
  fs.appendFileSync(logFile, logLine + '\n');
}

// ==================== OKKI CLI 执行 ====================

/**
 * 执行 OKKI CLI 命令
 */
function execOkkiCli(args = [], options = {}) {
  return new Promise((resolve, reject) => {
    // --json 是全局标志，需要放在子命令之前
    const fullArgs = [CONFIG.okkiCliPath, '--json', ...args];
    
    execFile('python3', fullArgs, {
      timeout: options.timeout || 10000,
      env: { ...process.env, PYTHONIOENCODING: 'utf-8' }
    }, (error, stdout, stderr) => {
      if (error) {
        reject(new Error(`OKKI CLI 执行失败：${error.message}\n${stderr}`));
        return;
      }
      
      try {
        const result = JSON.parse(stdout);
        resolve(result);
      } catch (e) {
        resolve({ raw: stdout, stderr });
      }
    });
  });
}

// ==================== 同步记录管理 ====================

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

/**
 * 保存同步记录
 */
function saveSyncRecord(draftId, metadata = {}) {
  try {
    const records = loadSyncRecords();
    records[draftId] = {
      synced_at: new Date().toISOString(),
      ...metadata
    };
    fs.writeFileSync(CONFIG.syncRecordFile, JSON.stringify(records, null, 2));
    return true;
  } catch (e) {
    log(`保存同步记录失败：${e.message}`, 'WARN');
    return false;
  }
}

/**
 * 检查草稿是否已同步
 */
function isSynced(draftId) {
  const records = loadSyncRecords();
  return !!records[draftId];
}

// ==================== 客户匹配 ====================

/**
 * 从邮箱地址提取域名
 */
function extractDomain(email) {
  if (!email || typeof email !== 'string') {
    return null;
  }
  const match = email.match(/@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/);
  return match ? match[1].toLowerCase() : null;
}

/**
 * 公共域名黑名单
 */
const PUBLIC_DOMAINS = [
  'gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com',
  'qq.com', '163.com', '126.com', 'sina.com', 'sohu.com',
  'icloud.com', 'me.com', 'mac.com', 'live.com', 'msn.com'
];

/**
 * 检查域名是否为公共域名
 */
function isPublicDomain(domain) {
  if (!domain) return true;
  return PUBLIC_DOMAINS.includes(domain.toLowerCase());
}

/**
 * 通过域名搜索 OKKI 客户
 */
async function searchCustomerByDomain(domain) {
  if (!domain || isPublicDomain(domain)) {
    return null;
  }
  
  try {
    const result = await execOkkiCli(['company', 'list', '-k', domain, '-l', '5']);
    
    if (result.data && result.data.length > 0) {
      // 精确匹配域名
      const exactMatch = result.data.find(company => {
        const companyDomain = extractDomain(company.website || company.email || '');
        return companyDomain === domain;
      });
      
      if (exactMatch) {
        return {
          company_id: exactMatch.company_id || exactMatch.id,
          name: exactMatch.name,
          match_type: 'domain_exact',
          confidence: 0.95
        };
      }
      
      // 部分匹配
      if (result.data.length > 0) {
        return {
          company_id: result.data[0].company_id || result.data[0].id,
          name: result.data[0].name,
          match_type: 'domain_partial',
          confidence: 0.7
        };
      }
    }
    
    return null;
  } catch (e) {
    log(`域名搜索失败：${e.message}`, 'WARN');
    return null;
  }
}

/**
 * 通过客户名称搜索 OKKI 客户
 */
async function searchCustomerByName(name) {
  if (!name) {
    return null;
  }
  
  try {
    const result = await execOkkiCli(['company', 'list', '-k', name, '-l', '5']);
    
    if (result.data && result.data.length > 0) {
      // 精确匹配名称
      const exactMatch = result.data.find(company => 
        company.name.toLowerCase() === name.toLowerCase()
      );
      
      if (exactMatch) {
        return {
          company_id: exactMatch.company_id || exactMatch.id,
          name: exactMatch.name,
          match_type: 'name_exact',
          confidence: 0.95
        };
      }
      
      // 部分匹配
      return {
        company_id: result.data[0].company_id || result.data[0].id,
        name: result.data[0].name,
        match_type: 'name_partial',
        confidence: 0.7
      };
    }
    
    return null;
  } catch (e) {
    log(`名称搜索失败：${e.message}`, 'WARN');
    return null;
  }
}

/**
 * 匹配客户（优先使用 customer_id，其次邮箱域名，最后名称）
 */
async function matchCustomer(draft) {
  // 如果 draft 已经有 OKKI company_id，直接使用
  if (draft.okki_company_id) {
    log(`使用已有 OKKI 客户 ID：${draft.okki_company_id}`);
    return {
      company_id: draft.okki_company_id,
      name: draft.customer_name,
      match_type: 'existing_id',
      confidence: 1.0
    };
  }
  
  // 尝试通过邮箱域名匹配
  if (draft.customer_email) {
    const domain = extractDomain(draft.customer_email);
    if (domain && !isPublicDomain(domain)) {
      const domainResult = await searchCustomerByDomain(domain);
      if (domainResult) {
        return domainResult;
      }
    }
  }
  
  // 尝试通过客户名称匹配
  if (draft.customer_name) {
    const nameResult = await searchCustomerByName(draft.customer_name);
    if (nameResult) {
      return nameResult;
    }
  }
  
  return null;
}

// ==================== 创建跟进记录 ====================

/**
 * 创建 OKKI 跟进记录
 */
async function createFollowUpTrail(companyId, draft) {
  // 构建跟进内容
  const stageLabels = {
    'new_inquiry': '新询盘',
    'quoted': '已报价',
    'sample_sent': '已寄样',
    'negotiating': '谈判中',
    'closed_won': '成交',
    'lost': '流失'
  };
  
  const stageLabel = stageLabels[draft.stage] || draft.stage;
  const followUpDay = draft.follow_up_day || 0;
  
  let content = `跟进提醒：${stageLabel}阶段\n`;
  content += `客户：${draft.customer_name}\n`;
  content += `邮箱：${draft.customer_email || '(未提供)'}\n`;
  content += `跟进类型：${draft.intent || '常规跟进'}\n`;
  content += `跟进时机：第${followUpDay}天\n`;
  
  if (draft.template_id) {
    content += `使用模板：${draft.template_id}\n`;
  }
  
  if (draft.subject) {
    content += `邮件主题：${draft.subject}\n`;
  }
  
  content += `\n[自动生成的跟进草稿，待审核发送]`;
  
  try {
    const result = await execOkkiCli([
      'trail', 'add',
      '--company', companyId,
      '--content', content,
      '--type', CONFIG.TRAIL_TYPE.FOLLOW_UP.toString()
    ]);
    
    if (result.error) {
      throw new Error(result.error);
    }
    
    return {
      success: true,
      company_id: companyId,
      trail_id: result.trail_id || result.data?.trail_id,
      action: 'trail.add'
    };
  } catch (e) {
    return {
      success: false,
      reason: 'api_error',
      message: e.message,
      company_id: companyId
    };
  }
}

// ==================== 草稿文件处理 ====================

/**
 * 读取所有待同步的草稿
 */
function loadPendingDrafts() {
  const drafts = [];
  
  if (!fs.existsSync(CONFIG.draftsDir)) {
    log(`草稿目录不存在：${CONFIG.draftsDir}`, 'WARN');
    return drafts;
  }
  
  const files = fs.readdirSync(CONFIG.draftsDir);
  
  for (const file of files) {
    if (!file.startsWith('draft-') || !file.endsWith('.json')) {
      continue;
    }
    
    const filepath = path.join(CONFIG.draftsDir, file);
    
    try {
      const draft = JSON.parse(fs.readFileSync(filepath, 'utf8'));
      
      // 只处理 status 为 draft 的草稿
      if (draft.status === 'draft') {
        drafts.push({ filepath, ...draft });
      }
    } catch (e) {
      log(`读取草稿文件失败 ${file}: ${e.message}`, 'WARN');
    }
  }
  
  return drafts;
}

/**
 * 更新草稿状态
 */
function updateDraftStatus(draft, newStatus, okkiTrailId = null) {
  const updatedDraft = {
    ...draft,
    status: newStatus,
    updated_at: new Date().toISOString()
  };
  
  if (okkiTrailId) {
    updatedDraft.okki_trail_id = okkiTrailId;
  }
  
  fs.writeFileSync(draft.filepath, JSON.stringify(updatedDraft, null, 2), 'utf8');
  log(`草稿状态更新：${draft.draft_id} → ${newStatus}`);
}

// ==================== 主同步逻辑 ====================

/**
 * 执行同步
 */
async function runSync(options = {}) {
  const { dryRun = false, draftIds = null } = options;
  
  log('='.repeat(60));
  log('OKKI Integration - 跟进草稿同步');
  log(`模式：${dryRun ? 'DRY-RUN' : 'LIVE'}`);
  log('='.repeat(60));
  
  const results = {
    total_drafts: 0,
    synced: 0,
    skipped: 0,
    failed: 0,
    details: []
  };
  
  try {
    // 1. 加载待同步草稿
    let drafts = loadPendingDrafts();
    
    // 如果指定了 draftIds，只处理这些
    if (draftIds && draftIds.length > 0) {
      drafts = drafts.filter(d => draftIds.includes(d.draft_id));
    }
    
    results.total_drafts = drafts.length;
    log(`找到 ${drafts.length} 个待同步草稿`);
    
    if (drafts.length === 0) {
      log('没有需要同步的草稿');
      return { success: true, ...results };
    }
    
    log('-'.repeat(60));
    
    // 2. 逐个处理草稿
    for (const draft of drafts) {
      log(`处理草稿：${draft.draft_id}`);
      log(`  客户：${draft.customer_name} (${draft.customer_email || '无邮箱'})`);
      log(`  阶段：${draft.stage}`);
      log(`  模板：${draft.template_id}`);
      
      // 检查是否已同步
      if (isSynced(draft.draft_id)) {
        log(`  跳过：已同步`);
        results.skipped++;
        results.details.push({
          draft_id: draft.draft_id,
          status: 'skipped',
          reason: 'already_synced'
        });
        continue;
      }
      
      if (dryRun) {
        log(`  [DRY-RUN] 将创建 OKKI 跟进记录`);
        results.synced++;
        results.details.push({
          draft_id: draft.draft_id,
          status: 'dry_run',
          customer: draft.customer_name
        });
        continue;
      }
      
      // 3. 匹配客户
      const customer = await matchCustomer(draft);
      
      if (!customer) {
        log(`  失败：未找到匹配的 OKKI 客户`, 'ERROR');
        results.failed++;
        results.details.push({
          draft_id: draft.draft_id,
          status: 'failed',
          reason: 'customer_not_found',
          customer_name: draft.customer_name,
          customer_email: draft.customer_email
        });
        continue;
      }
      
      log(`  匹配客户：${customer.name} (${customer.company_id}) [${customer.match_type}, confidence=${customer.confidence}]`);
      
      // 4. 创建跟进记录
      const trailResult = await createFollowUpTrail(customer.company_id, draft);
      
      if (trailResult.success) {
        log(`  ✓ OKKI 跟进记录创建成功：${trailResult.trail_id}`);
        
        // 5. 更新草稿状态
        updateDraftStatus(draft, 'synced', trailResult.trail_id);
        
        // 6. 记录同步
        saveSyncRecord(draft.draft_id, {
          okki_company_id: customer.company_id,
          okki_trail_id: trailResult.trail_id,
          match_type: customer.match_type
        });
        
        results.synced++;
        results.details.push({
          draft_id: draft.draft_id,
          status: 'synced',
          customer: draft.customer_name,
          okki_company_id: customer.company_id,
          okki_trail_id: trailResult.trail_id
        });
      } else {
        log(`  失败：${trailResult.reason} - ${trailResult.message}`, 'ERROR');
        results.failed++;
        results.details.push({
          draft_id: draft.draft_id,
          status: 'failed',
          reason: trailResult.reason,
          message: trailResult.message
        });
      }
    }
    
    // 3. 输出摘要
    log('='.repeat(60));
    log('同步完成摘要');
    log('='.repeat(60));
    log(`草稿总数：${results.total_drafts}`);
    log(`同步成功：${results.synced}`);
    log(`跳过：${results.skipped}`);
    log(`失败：${results.failed}`);
    
    if (dryRun) {
      log('');
      log('DRY-RUN 模式：未实际创建 OKKI 记录');
    }
    
    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'),
    draftIds: []
  };
  
  // 解析指定的 draft IDs
  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--draft' && args[i + 1]) {
      options.draftIds.push(args[i + 1]);
      i++;
    }
  }
  
  runSync(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 = {
  runSync,
  loadPendingDrafts,
  matchCustomer,
  createFollowUpTrail,
  updateDraftStatus,
  loadSyncRecords,
  saveSyncRecord,
  isSynced,
  CONFIG
};

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


path: test/e2e.sh
content: #!/bin/bash

# Follow-up Engine - End-to-End Test Script
# 测试跟进引擎完整流程

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
CONFIG_DIR="$SKILL_DIR/config"
DRAFTS_DIR="$SKILL_DIR/drafts"
LOGS_DIR="$SKILL_DIR/logs"

echo "========================================"
echo "Follow-up Engine E2E Test"
echo "========================================"
echo ""

# 颜色定义
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# 测试计数器
PASSED=0
FAILED=0

# 测试函数
test_pass() {
    echo -e "${GREEN}✓${NC} $1"
    ((PASSED++))
}

test_fail() {
    echo -e "${RED}✗${NC} $1"
    ((FAILED++))
}

test_info() {
    echo -e "${YELLOW}ℹ${NC} $1"
}

# 1. 验证配置文件
echo "【测试 1】验证配置文件..."
for config in follow-up-rules.json stage-transitions.json follow-up-strategies.json; do
    if [ -f "$CONFIG_DIR/$config" ]; then
        if node -e "JSON.parse(require('fs').readFileSync('$CONFIG_DIR/$config'))" 2>/dev/null; then
            test_pass "$config 格式有效"
        else
            test_fail "$config JSON 格式错误"
        fi
    else
        test_fail "$config 文件不存在"
    fi
done
echo ""

# 2. 验证脚本文件
echo "【测试 2】验证脚本文件..."
for script in follow-up-scheduler.js okki-integration.js; do
    if [ -f "$SKILL_DIR/scripts/$script" ]; then
        test_pass "$script 存在"
    else
        test_fail "$script 文件不存在"
    fi
done
echo ""

# 3. 测试 follow-up-scheduler.js (dry-run)
echo "【测试 3】测试 follow-up-scheduler.js (dry-run)..."
if node "$SKILL_DIR/scripts/follow-up-scheduler.js" --dry-run > /tmp/scheduler-test.log 2>&1; then
    test_pass "follow-up-scheduler.js dry-run 执行成功"
    test_info "日志：/tmp/scheduler-test.log"
else
    test_fail "follow-up-scheduler.js dry-run 执行失败"
    cat /tmp/scheduler-test.log
fi
echo ""

# 4. 测试 okki-integration.js (dry-run)
echo "【测试 4】测试 okki-integration.js (dry-run)..."
if node "$SKILL_DIR/scripts/okki-integration.js" --dry-run > /tmp/integration-test.log 2>&1; then
    test_pass "okki-integration.js dry-run 执行成功"
    test_info "日志：/tmp/integration-test.log"
else
    test_fail "okki-integration.js dry-run 执行失败"
    cat /tmp/integration-test.log
fi
echo ""

# 5. 验证目录结构
echo "【测试 5】验证目录结构..."
for dir in config scripts drafts logs; do
    if [ -d "$SKILL_DIR/$dir" ]; then
        test_pass "$dir/ 目录存在"
    else
        test_fail "$dir/ 目录不存在"
    fi
done
echo ""

# 6. 检查依赖
echo "【测试 6】检查依赖..."
if [ -f "/Users/wilson/.openclaw/workspace/xiaoman-okki/api/okki.py" ]; then
    test_pass "OKKI CLI 存在"
else
    test_fail "OKKI CLI 不存在"
fi

if [ -d "/Users/wilson/.openclaw/workspace/skills/imap-smtp-email" ]; then
    test_pass "email-smart-reply skill 存在"
else
    test_fail "email-smart-reply skill 不存在"
fi
echo ""

# 测试结果汇总
echo "========================================"
echo "测试结果汇总"
echo "========================================"
echo -e "${GREEN}通过：$PASSED${NC}"
echo -e "${RED}失败：$FAILED${NC}"
echo ""

if [ $FAILED -eq 0 ]; then
    echo -e "${GREEN}✓ 所有测试通过！${NC}"
    exit 0
else
    echo -e "${RED}✗ 部分测试失败，请检查日志${NC}"
    exit 1
fi


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