# Phase 6 · 私信中心 DM Center · Implementation Plan (技术大纲)

> 部分代码可借鉴 catclaw `dm-collector` 的队列模式，但 spec 要求加 AI 起草 + 人工审核 + 漏斗追踪等增量功能。

**Goal:** 实装 AI 起草 + 人工审核 + 速率受控 + 模板库 + 风控规避 + 漏斗追踪的完整 DM 工作流。

**Architecture:** in-memory 队列 + 持久化双写 → 速率限制器（每小时 12 / 每条 3-7 分钟随机间隔）→ Playwright 子进程发送 → Cookie 健康检测 → captcha 触发 12h 暂停。

**Tech Stack:** Anthropic SDK + OpenAI SDK + Qwen-compatible OpenAI SDK + bullmq-style in-memory queue + bridge.executeJS

---

## File Structure

```
packages/core/src/dm-center/
  ├── queue.ts                    (in-memory + persisted FIFO + delay)
  ├── rate-limiter.ts             (12/h, 3-7min interval, work hours, 14d per-user gap)
  ├── risk-monitor.ts             (cookie 健康 + captcha 检测 + auto pause)
  ├── ai-drafter.ts               (3 Provider 切换 + 模板变量替换)
  ├── template-engine.ts          (CRUD + 变量插槽 + A/B 变体)
  ├── follow-up-strategy.ts       (4h 无回 / 24h 已读未回触发跟进)
  ├── send-task.ts                (Playwright 实际发送)
  └── funnel-analytics.ts         (漏斗 + 转化率计算)

packages/core/src/ai-orchestrator/
  ├── anthropic.ts                (Claude SDK 封装)
  ├── openai.ts                   (GPT-4o)
  ├── qwen.ts                     (Qwen Plus，OpenAI 兼容)
  └── provider.ts                 (Provider 接口 + 切换路由)

packages/db/src/migrations/
  └── 006-dm.sql                  (dms / dm_templates / dm_accounts)

packages/db/src/repositories/
  ├── dms.ts
  ├── dm-templates.ts
  └── dm-accounts.ts

packages/ipc-contract/src/
  └── dm.ts

apps/desktop/src/pages/DM/
  ├── index.tsx                   (三栏布局)
  ├── components/
  │   ├── ConversationList.tsx
  │   ├── ConversationView.tsx
  │   ├── AIDraftBanner.tsx       (核心)
  │   ├── TemplateLibrary.tsx
  │   ├── TemplateEditor.tsx      (变量插槽 + 实时预览)
  │   ├── StrategyPanel.tsx
  │   ├── FunnelDashboard.tsx
  │   ├── BulkSendModal.tsx
  │   └── RiskAlertModal.tsx
  └── stores/dmStore.ts
```

---

## Task 列表

| # | 主题 | 关键代码 sketch |
|---|---|---|
| 1 | DB schema: dms / dm_templates / dm_accounts | `006-dm.sql` |
| 2 | Repositories | DRY 同 Phase 5 |
| 3 | RateLimiter (3 维: hour_max / interval / work_hours) | 见下 sketch |
| 4 | DM Queue (FIFO + delay enqueue) | bullmq-style in-memory |
| 5 | RiskMonitor (cookie 检测 / captcha 检测 / auto pause 12h) | bridge.executeJS 检测 |
| 6 | AI Drafter 3 Provider | Provider 接口 + 模板插槽替换 |
| 7 | TemplateEngine (CRUD / 变量 / A/B 变体 / 历史回复率) | dm_templates 表 |
| 8 | FollowUpStrategy (4h 无回 / 24h 已读未回 触发) | setInterval scan dms timeline |
| 9 | SendTask (Playwright 子进程实际发送) | bridge.executeJS 模拟 typing 3-7s |
| 10 | FunnelAnalytics (drafted → approved → queued → sent → read → replied → outcome) | timeline_json 解析 + 聚合 |
| 11 | DMContract IPC | listConversations / draftMessage / approveMessage / etc. |
| 12 | Renderer 三栏 UI | spec design dm.html |
| 13 | AI 起草 banner | streaming 显示 |
| 14 | Template Editor with 变量预览 | 实时预览 |
| 15 | FunnelDashboard | 4 步漏斗 + 模板对比 + 时段分析 |
| 16 | BulkSendModal (从 Explorer 跳过来) | 12 条逐条审核 / 全部采纳 |
| 17 | RiskAlertModal (红色 danger) | 12h 暂停 + 切换备用账号 |

## 关键代码 Sketch

### Rate Limiter

```typescript
// packages/core/src/dm-center/rate-limiter.ts
export class RateLimiter {
  constructor(
    private opts: {
      hourMax: number;        // 12
      intervalMinSec: number; // 180
      intervalMaxSec: number; // 420
      workHourStart: number;  // 9
      workHourEnd: number;    // 23
      perUserGapDays: number; // 14
    },
    private dms: DMsRepo,
  ) {}

  /** 计算下一条 DM 可以发送的时间 */
  nextSendTime(): Date {
    // 1. 当日已发数 vs hourMax
    const sentLastHour = this.dms.countSentInLastHour();
    if (sentLastHour >= this.opts.hourMax) {
      return new Date(Date.now() + 60 * 60 * 1000);  // 等下一小时
    }
    // 2. 上一条发送时间 + 随机间隔
    const lastSentAt = this.dms.lastSentAt() ?? 0;
    const interval = (this.opts.intervalMinSec + Math.random() * (this.opts.intervalMaxSec - this.opts.intervalMinSec)) * 1000;
    const candidate = new Date(lastSentAt + interval);
    // 3. 卡到工作时段
    return this.clampToWorkHours(candidate);
  }

  canSendToUser(userId: string): boolean {
    const lastSentToUser = this.dms.lastSentTo(userId);
    if (!lastSentToUser) return true;
    return Date.now() - lastSentToUser > this.opts.perUserGapDays * 86400 * 1000;
  }

  private clampToWorkHours(d: Date): Date {
    // ...
  }
}
```

### Risk Monitor

```typescript
// packages/core/src/dm-center/risk-monitor.ts
export class RiskMonitor {
  /** 每次发送前 + 每 5 分钟一次主动检测 */
  async check(bridge: BrowserBridge): Promise<{ healthy: boolean; reason?: string }> {
    // 1. cookie 是否有效（看 /api/foryou 返回）
    const apiCheck = await bridge.executeJS(`fetch('/api/foryou').then(r => r.status)`);
    if (apiCheck !== 200) return { healthy: false, reason: 'cookie_expired' };

    // 2. 当前页面是否有 captcha 元素
    const captcha = await bridge.executeJS(`!!document.querySelector('.captcha-verify-container, iframe[src*="captcha"]')`);
    if (captcha) return { healthy: false, reason: 'captcha_detected' };

    // 3. 是否被风控（页面跳到登录或被弹"网络异常"）
    const url = await bridge.executeJS(`location.href`);
    if (url.includes('/login') || url.includes('/error')) return { healthy: false, reason: 'redirected' };

    return { healthy: true };
  }

  /** 触发风控 → 暂停发送 12h + 推 alert event */
  trigger(reason: string, accountId: string): void {
    this.dmAccountsRepo.update(accountId, { paused_until: Date.now() + 12 * 3600 * 1000, last_alert: reason });
    this.eventBus.emit('dm.risk_alert', { accountId, reason });
  }
}
```

### AI Drafter 3-Provider

```typescript
// packages/core/src/dm-center/ai-drafter.ts
export class AIDrafter {
  constructor(private provider: AIProvider) {}

  async draft(input: {
    template: DMTemplate;
    broadcaster: Broadcaster;       // 含 7 维度评分 / AI 评语 / 视频内容
    studio: { name: string; pitch: string };
    type: 'first_invite_warm' | 'first_invite_direct' | 'follow_up' | 'interview';
  }): Promise<string> {
    const prompt = `你是 TikTok 团播运营。请基于模板和主播信息，写一条个性化的私信。
模板骨架：${input.template.content}
主播信息：
- 昵称：${input.broadcaster.display}
- 风格：${input.broadcaster.tags.join(', ')}
- AI 评语：${input.broadcaster.scores.summary}
- 近 7d 增长：${input.broadcaster.scores.growth_7d}%
工作室：${input.studio.name} - ${input.studio.pitch}

要求：
- 100-150 字
- 自然、温和、不像群发
- 引用主播视频里的 1-2 个具体细节（从 AI 评语提取）
- 末尾留 1 个开放性问题诱发回复
直接输出私信内容，不要解释。`;
    return await this.provider.complete(prompt, { temperature: 0.7, max_tokens: 200 });
  }
}
```

### Funnel Analytics

```typescript
// packages/core/src/dm-center/funnel-analytics.ts
export class FunnelAnalytics {
  constructor(private dms: DMsRepo) {}

  computeFunnel(days: number): {
    drafted: number;
    sent: number;
    read: number;
    replied: number;
    signed: number;
  } {
    const since = Date.now() - days * 86400 * 1000;
    return {
      drafted: this.dms.countByEvent('drafted', since),
      sent: this.dms.countByEvent('sent', since),
      read: this.dms.countByEvent('read', since),
      replied: this.dms.countByEvent('replied', since),
      signed: this.dms.countByOutcome('signed', since),
    };
  }

  templateEffectiveness(): { template: string; reply_rate: number; usage_count: number }[] {
    return this.dms.groupByTemplateWithReplyRate();
  }

  bestSendTime(): { hour: number; reply_rate: number }[] {
    return this.dms.replyRateByHour();
  }
}
```

---

## 准入条件

- ✅ Phase 5 已 merge（探索池有数据 + Backstage 框架已就位）

## 准出条件

- ✅ 选 1 主播 → 选「首次邀约·温和」模板 → AI 起草 → 采纳 → 进队列 → 3-7 分钟后实际通过 bridge 发出
- ✅ 队列发完 12 条触发自动暂停（接近上限），主持人可手动解锁
- ✅ 漏斗页正确计算 30 天的转化率
- ✅ 检测到 captcha 立即停 + 弹红色 alert
- ✅ 多账号管理：3 账号独立 cookie + 独立速率
- ✅ 模板 A/B 变体随机选 + 自动跟踪回复率

## 关键技术决策

| 决策 | 理由 |
|---|---|
| 队列 in-memory + 持久化双写 | bullmq 太重；自实现简单 + 重启可恢复 |
| 速率限制 3 维 + 14 天 per-user gap | 比 catclaw 严格（catclaw 只有间隔，没 per-user）|
| Risk monitor 主动 + 被动检测 | catclaw 仅被动（发失败才知道），增加主动 |
| AI Drafter 3 Provider 切换 | 用户选 Provider，按场景默认（DM=Claude, Vision=GPT-4o, 中文=Qwen） |

## Self-Review

- ✅ 涵盖 spec §3 DM 全部 + design dm.html 9 屏 + 3 弹窗
- ✅ 区分清楚：catclaw 现有 dm-collector 是简单队列，本 phase 增加 AI 起草 / 漏斗 / 风控规避
- ✅ DRY: 复用 Phase 5 的 Backstage 框架（bridge.executeJS 体系）
