# Spec: 商機雷達＋成交 CRM

> Status: `approved`  
> Status note: 使用者 2026-07-31「好」（spec 通過）  
> Source: `docs/product/demand-radar/requirements.md`（**approved** 2026-07-31）  
> Last updated: `2026-08-07`  
> 底座：`docs/product/haixun-backend/spec.md`（海巡雙路徑、Outbox、用量 BYOK）、`docs/product/growth-loop/spec.md`（OutcomeEvent 歸因、AccountHealth 送出閘）；本 run 為**加值層**  
> 決策對齊：requirements §6 命名決策、§7 決策 #1–#12

## 1. 摘要

本規格定義「服務者每天收到一份『正在找你的人』名單，並追到成交」的**對外行為契約**。

**P0：** 服務檔案 → 雷達訂閱與每日自動巡 → 商機五問判定與意向分數 → 今日商機頁 → AI 回覆五版本 → 聯絡人與 CRM 七階段（＋待追蹤標記）→ 追蹤提醒 → 轉換統計 → 方案配額掛載。
**P1：** 手動匯入商機、報價與成交金額、價格帶校準。
**P2（只鎖邊界）：** LINE 官方帳號、Google 商家評論、網站表單、自有留言私訊收斂、空檔媒合。
**不做：** 重做海巡抓取與發文主流程、重做付費流程、重量級 CRM、自動判定成交。

## 2. 名詞與實體

| 名詞 | 定義 |
|------|------|
| **ServiceProfile** | 服務檔案：服務項目、價格區間、代表案例、**不能說的內容**、常見問題、服務地區、可接案空檔。判定與回覆生成的共同輸入 |
| **RadarWatch** | 雷達訂閱：常駐關鍵字監控設定（關鍵字、地區條件、排除詞、啟用狀態） |
| **RadarSweep** | 一次每日巡的執行紀錄：對應一個 Job，記錄掃描詞、命中數、判定數、成本與截斷情形 |
| **Opportunity** | 商機：一筆經五問判定、值得跟進的需求訊號，帶 `intent_score`、`intent_band`、判定理由 |
| **ReplyVariant** | 商機下的一個回覆版本（公開留言／私訊／不帶銷售感／專業／幽默） |
| **Contact** | 聯絡人：跨貼文聚合的人，鍵為（來源平台＋作者帳號＋擁有者） |
| **ContactTouch** | 聯絡人時間軸上的一則接觸紀錄（狀態變更、送出回覆、備註、成交回報） |
| **FollowUp** | 追蹤提醒：到期未回應時進站內通知，可產生 AI 追蹤訊息 |
| **五問** | 需求真實性、購買意圖、地區符合、需求時效、服務匹配度 |

**沿用底座（不重定義）：** Member、ThreadsAccount、ScoutPost／`outreach_status`、Brand／Product、OutboxBundle/Step、Persona、Usage/Plan（Free／Starter／Pro）、Job、Notification、OutcomeEvent／AttributionLink、AccountHealth。

**命名紅線：** 全域禁用 `lead` 指稱銷售線索（既有語意為 ThreadPlay 主帳號）。

## 3. 狀態機

### 3.1 RadarWatch

```text
active --> paused      : 使用者暫停（不再排入每日巡）
paused --> active      : 恢復
active|paused --> archived : 軟刪；歷史商機保留
archived --> (deleted)    : 使用者確認後永久刪除設定；歷史資料保留
```

- `archived` 的 watch 不再產生新商機，但既有商機與統計**不刪除**。
- `archived` 可另行永久刪除 watch 設定；此動作只移除設定本身，不刪除 `radar_sweeps`、`radar_opportunities`、回覆或統計。`active`／`paused` 不得直接永久刪除。
- 超過方案 watch 上限時**禁止**新增 `active`（明確錯誤），既有超額者維持不變不強制降級。

### 3.2 Opportunity（判定與收單）

```text
(sweep 命中) --> judging      : 進入五問判定
judging --> qualified          : 通過硬否決，得 intent_score 與 band
judging --> rejected           : 硬否決（非真實需求／同業叫賣／逾期／地區明確不符且不可遠端）
qualified --> accepted         : 使用者「加入名單」→ 綁定或建立 Contact
qualified --> dismissed        : 使用者略過
rejected --> qualified         : 使用者手動覆寫（記錄覆寫）
accepted --> (CRM 由 Contact 接手)
```

- **硬否決條件（不計分直接 `rejected`）：** 五問第一問「是不是真實需求」為否；既有分類為 `provider_offer`／`announcement`／`noise`；貼文發布距今 > 14 天。
- `rejected` **仍入庫可查**（避免誤殺無法回溯），但預設不出現在今日商機頁。
- 使用者覆寫 `intent_band` 或把 `rejected` 拉回 `qualified` 時，寫入 `override`（原值、新值、時間、操作者）——這是後續校準判定的素材。

### 3.3 意向分數與分級

```text
intent_score = 需求真實性 30 + 購買意圖 30 + 地區符合 15 + 需求時效 15 + 服務匹配度 10
band: high >= 80 | mid 50–79 | low < 50
```

- 每個維度必須輸出**分數與人話理由**；`reasons[]` 缺任一維度不得入庫。
- 地區三態：`match`／`mismatch`／`unknown`。`unknown` 得部分分（不等同 `mismatch`），且**禁止猜測縣市**。ServiceProfile 標記可遠端服務時，`mismatch` 不觸發硬否決且地區維度給滿分。
- 時效：距今 ≤ 24h 給滿分，24–72h 遞減，72h–14 天低分，> 14 天硬否決。

### 3.4 Contact CRM 階段

七個互斥階段 ＋ 一個並存標記。畫面以單一工作清單呈現，`needs_follow_up` 作篩選／badge，不另複製成第八張卡（2026-08-12 使用性修訂）。

```text
stage: new_found --> engaged --> dm_sent --> replied --> quoted --> won | lost
```

- **允許任意跳轉**（含回退、跳級）；每次轉移寫一筆 `ContactTouch`（來源階段、目標階段、時間、操作者）。
- `won`／`lost` 為終態但**可重新開啟**（轉回任一階段，同樣留痕）。
- `needs_follow_up` 是 **boolean 標記**，可與任何階段並存，非階段值。
- `won` 必須由人確認；**系統永不自動判定成交**。

### 3.5 FollowUp

```text
scheduled --> notified   : 到期 → 站內通知
notified --> done        : 使用者處理（回覆／推進階段／手動關閉）
notified --> snoozed     : 延後 N 天 → 回到 scheduled
notified --> escalated   : 已通知達上限次數 → 建議轉 lost（僅建議，需人確認）
```

- 預設：無回應 **3 天**觸發，最多通知 **2** 次，天數可由使用者調整。
- `escalated` **不自動改階段**、不自動關閉。

## 4. 使用者流程（行為）

### 4.1 服務檔案（P0）

1. 使用者填寫：`services[]`（項目＋價格區間）、`cases[]`、`forbidden[]`（不能說的內容）、`faq[]`、`service_areas[]`（縣市代碼；可勾「可遠端」）、`availability`（可接案空檔文字）、`tone_note`（語氣備註）。
2. 服務檔案**每會員一份**（非多份），可隨時修改；修改不回溯既有商機的判定結果。
3. 未建立服務檔案時：**禁止**建立 `active` 的 RadarWatch，並在雷達頁引導先建檔（避免判定缺乏依據而產生垃圾名單）。
4. `forbidden[]` 為回覆生成的**硬性過濾**（§4.5）。

### 4.2 雷達訂閱與每日自動巡（P0）

1. 使用者建立 RadarWatch：`terms[]`（關鍵字）、`exclude_terms[]`、`regions[]`（可留空＝沿用服務檔案）、`enabled`。
2. 系統可依服務檔案**建議關鍵字**（`POST /api/v1/radar/watches/suggest`），使用者可全採用或逐條增刪。這是 P0 上手體驗的一部分（requirements 開放問題 #1）。
3. **每日排程：** 每日 **UTC 22:00**（＝台北隔日 06:00）為所有 `active` watch 建立 `radar_sweep` Job，讓使用者早晨打開即有名單。多台 worker 以既有 Redis lock 模式確保只跑一台。時區個人化為 P2。
4. 巡的路徑**沿用既有海巡雙路徑分流**：會員 `dev_mode_enabled=false` 走 API 路徑，`true` 走瀏覽器工作階段路徑。**不新增第三種抓取方式。**
5. 命中 → 逐筆五問判定（§4.3）→ 寫入 Opportunity。
6. **配額截斷：** 當日商機數達方案上限時，依 `intent_score` 由高至低保留，其餘不建立，並在 RadarSweep 記 `truncated_count`；今日頁明確提示「已達今日上限，N 筆較低意向未收錄」。**不得靜默丟棄。**
7. 使用者可對單一 watch 手動觸發即時巡（`POST /api/v1/radar/watches/:id/sweep`），受每日配額同一個池子約束。
8. **降級：** 抓取路徑不可用時，Sweep 記 `failed` 與原因並發通知；**禁止**回成功空資料。

### 4.3 五問判定（P0）

1. 輸入：貼文全文與中繼資料（作者、發布時間、permalink）、ServiceProfile、觸發的 watch 關鍵字。
2. 沿用既有分類結果（`seeking_help`／`seeking_recommendation`／`provider_offer`／`discussion`／`asking`／`announcement`／`noise`）作為第一問的主要依據，**不重做分類器**。
3. 輸出：`intent_score`、`intent_band`、`reasons[]`（五維度各一條人話理由）、`region_detected`、`region_match`、`freshness_hours`、`matched_service`（對到哪個服務項目）。
4. 硬否決者寫 `rejected` 與 `reject_reason`，仍入庫。
5. 判定計入既有 `ai_research` meter，`source` 標 `radar.judge`（§5.5）。

### 4.4 今日商機頁（P0）

1. 頂部統計：`今日找到 N 筆`＋`高意向 A ・中意向 B ・低意向 C`。
2. 卡片欄位：原文摘要、`intent_score`、`intent_band` badge、`region_detected`、發布距今時間（由 `posted_at` 前端換算，後端只給 ns 時間）、**預設回覆（公開留言版）**。
3. 卡片動作：**開啟原文**（permalink 新分頁）、**加入名單**（→ `accepted`，綁定或建立 Contact）、**產生其他回覆**（→ 生成其他版本）、**略過**（→ `dismissed`）、**看判定理由**（展開 `reasons[]`）、**覆寫分級**。
4. 空狀態：當日 0 筆時**不得顯示空白**，須給明確原因（尚未到巡的時間／watch 全部暫停／關鍵字太窄的建議／抓取失敗）與下一步動作。
5. 低意向預設收合，可展開。

### 4.5 AI 回覆五版本（P0）

1. 版本枚舉：`public_comment`｜`dm`｜`no_sales`｜`professional`｜`humorous`。
2. **首次判定時只生成 `public_comment`**（成本控制，requirements 決策 #6）；其餘按需生成。
3. 輸入：Opportunity 原文、ServiceProfile（服務項目、價格區間、案例、FAQ、語氣備註）、active Persona（若有）。
4. **`forbidden[]` 硬性過濾：** 生成後檢查，命中則自動重生一次；仍命中則**不輸出**並回明確錯誤（非 102000），不得輸出違規內容。
5. 回覆生成計入既有 `ai_copy` meter，`source` 標 `radar.reply`。
6. **送出沿用既有管道**：公開留言版走既有海巡外展送出／Outbox；私訊版 P0 **僅提供複製**（Threads 無私訊 API），複製後由使用者手動送出並回頭標記。
7. 送出前**必經既有 AccountHealth 送出閘**：`warn` 帶警示、`throttle` 拒絕自動送出僅留手動路徑。本 run **不新增繞過** 護欄的路徑。

### 4.6 加入名單與 CRM（P0）

1. 「加入名單」時依（`source_platform`＋`author_handle`＋`owner_uid`）尋找既有 Contact：命中則掛上，未命中則建立，初始 `stage=new_found`。
2. 同一人多筆商機**收斂在同一 Contact 下**，時間軸可看到全部接觸歷史。
3. **跨平台不自動合併**；提供手動 `merge`／`unmerge`，合併保留兩邊時間軸。
4. CRM 列表以 Contact 為單位，可搜尋名稱／Threads 帳號，並依階段、`needs_follow_up`、意向分數、最後接觸時間篩選排序；固定分頁，待追蹤只作篩選，不重複畫同一位 Contact。
5. 階段推進、備註、標記待追蹤皆寫 `ContactTouch`。
6. 成交回報：填金額（選填）與備註 → 階段轉 `won` → **寫入既有 OutcomeEvent**（`source_type=radar_opportunity`，連 `opportunity_id`、`contact_id`），不另建成交帳。可事後修改／刪除並留痕。
7. Contact 可從工作名單「移除」：設 `removed_at`、關閉活躍 FollowUp，列表與詳情不再顯示；Opportunity、ContactTouch、OutcomeEvent 均保留。同一作者日後再次加入名單時恢復原 Contact，避免破壞唯一鍵與歷史鏈。

### 4.7 追蹤提醒（P0）

1. 商機送出回覆或階段進入 `engaged`／`dm_sent` 後，建立 FollowUp，`due_at = 最後接觸 + N 天`（預設 3）。
2. 日排程 `radar_followup_scan` 掃到期 → 站內通知（沿用既有 `notifications`，`ref_type=contact`）。
3. 通知落地頁為該 Contact 詳情；提供「產生 AI 追蹤訊息」（計入 `ai_copy`，`source` 標 `radar.followup`）。
4. 階段推進到 `replied` 以後或使用者手動關閉 → FollowUp `done`。
5. 通知達 2 次仍無動作 → `escalated`，在 CRM 顯示「建議轉未成交」，**需人確認**。

### 4.8 轉換統計（P0）

1. 維度：
   - **關鍵字轉換率**：以 watch 的 `term` 為單位，`won 數 / accepted 數`，附 accepted、replied、won 絕對數。
   - **回覆版本成功率**：以 `variant` 為單位，`(replied + won) / used`。
   - **成交來源**：radar／既有海巡／手動匯入 的成交筆數與金額。
2. **樣本不足不下結論：** 任一維度樣本 < 5 時只顯示原始數字並標「樣本不足」，不顯示排名或百分比結論（沿用既有 benchmark 的謹慎原則）。
3. 金額來源為手動回報（P1 起有金額維度），P0 先以筆數呈現。

### 4.9 方案配額（P0）

1. 配額維度：`max_active_watches`、`max_daily_opportunities`。AI 判定與回覆另計入既有點數 meter，不重複收費。
2. 建議值（plan 階段可調，行為鎖定）：

| 方案 | max_active_watches | max_daily_opportunities |
|------|--------------------|-------------------------|
| Free | 1 | 5 |
| Starter | 5 | 30 |
| Pro | 20 | 100 |

3. 超限行為：新增 watch 超限 → 明確錯誤並提示升級；當日商機超限 → §4.2 第 6 點截斷並提示，**每日巡不因此停止**。
4. 點數耗盡時：判定與回覆生成走既有 platform gate 的既有行為（BYOK 者不受平台額度限制）；**每日巡本身不因點數耗盡而刪除 watch**。

### 4.10 既有海巡命中升級為商機（P0）

1. 既有海巡命中列表新增動作「升級為商機」（後端 API 保留；前端需求／解法媒合 UI 併入商機後，話題頁可不顯示此鈕）。
2. **決策：複製，不共用**（requirements 開放問題 #5）。建立新的 Opportunity 並記 `source_scout_post_id` 反向連結，兩邊狀態機互不影響，避免 `outreach_status` 與 Opportunity 狀態互相污染。
3. 升級後跑同一套五問判定，進同一條 CRM。
4. 前端 **activity（話題）** 模式保留為 `/app/scout` 話題靈感；需求／解法媒合找客戶改走商機頁「立即探索」或每日訂閱巡。後端 scout 三模式與 Brand／Product 基礎設施仍保留。

### 4.12 立即探索與 Threads 短詞（2026-08-07）

1. **單一找需求管線：** 每日 watch 巡、立即探索、手動匯入皆經同一套五問判定進今日商機。
2. **`POST /api/v1/radar/explore`：** req `{ terms: string[] }`（1–6 組）；每組必須通過 Threads 短詞規則，不合規整組擋下不默默修正。同步執行：逐 term fan-out 搜尋（去重）→ 分類 → `ProcessCandidates`（每日配額、去重）→ 寫 `RadarSweep`（`watch_id` 可空）→ 回 `hit_count`／`judged_count`／`created_count`／`truncated_count`／`credits_used`。
3. **Threads 短詞規則：** 一組 ≤2 token（半形空格）、中文每詞 2–4 字、整組去掉空格 ≤12 字；禁標點／boolean／emoji／#。建議關鍵字、explore、話題工坊共用此規則；訂閱表單對不合規詞顯示警示（不硬擋既有資料）。
4. **每日巡 fan-out：** `SearchHitsOnly` 逐 term 搜尋再去重，禁止把整組 terms join 成一條 query（稀釋召回）。
5. **話題今日目標補抓：** 話題頁（`/app/scout`）設定今日目標後，掃描 brief 帶 `target_count`（剩餘則數，上限 40）。`RunScanFromBrief`：主路徑 fan-out 後若 hit < target，先同路徑加碼再搜；若主路徑為 crawler 仍不足，再以 search provider（Exa domain-restricted）只補缺口並 dedupe。不湊數無限重試；仍不足則以實際命中數入庫並讓 UI 顯示。
6. **話題自然語意：** activity 話題工坊接受自然輸入並保留整體意圖；工作方向組成「職種＋外包／接案／推薦」，作品／人物／事件保留原名並補熱門／最新查詢。前端預選排序最前的 3 組 fan-out；含多個主題核的 query 必須在正文同時命中各核心，熱門／最新等 discovery 修飾詞不要求正文逐字出現。crawler 同時命中 top＋recent（`track=both`）時優先於單軌結果。
7. **Scout 貼文去重：** 去重身分取 Threads permalink 的穩定 post shortcode（`/post/<code>` 或 `/t/<code>`），不得以完整 hostname／作者路徑作唯一判斷；`threads.net`／`threads.com`、子網域、分享參數與作者改名網址均視為同一篇。fan-out、補抓合併、持久化 ID、後端列表與前端保底使用同一語意；既有歷史重複在讀取時合併顯示但不自動刪除，優先保留已處理狀態。
8. **Scout 話題品質驗收（2026-08-10）：** `/app/scout` 的獨立 run、合格數驅動補量、canonical／歷史去重、原貼文時間排序與雙層分頁已完成；完整需求與逐條測試證據維護於 `docs/product/scout-topic-quality/`，不改變 Radar 的五問判定或 CRM 狀態機。

### 4.11 P1／P2 邊界

| 能力 | 波次 | 行為邊界 |
|------|------|----------|
| 手動匯入 | P1 | 貼 Threads／Facebook 貼文網址或 CSV → 建 Opportunity → 跑同一套判定；來源標 `manual_import` |
| 報價與成交金額 | P1 | `quoted`／`won` 可填金額幣別預設 TWD；寫既有 OutcomeEvent |
| 價格帶校準 | P1 | 依 P0 每會員雷達成本數據決定是否調 Starter／Pro 價格；本 run 不改價 |
| LINE 官方帳號 | P2 | 私訊建立／更新 Contact，進同一 CRM |
| Google 評論／網站表單 | P2 | 只做自有與公開來源，不碰第三方私有資料 |
| 空檔媒合 | P2 | 依 ServiceProfile `availability` 對高意向商機提示可約時段 |

## 5. 介面契約

### 5.1 共通

- 沿用 `haixun-backend`：成功 `code=102000`；`page`／`pageSize` → `pagination`＋`list`；時間 **unix nanoseconds UTC**；`uid` 數字。
- Auth：Bearer JWT；資源擁有者 = 登入 uid，全部 API 以 `owner_uid` 隔離。
- 後端一律 goctl：改 `generate/api/*.api` → `make gen-api` → 業務只寫 `internal/logic/**` 與 `internal/module/**`。
- **禁止未實作回成功空資料。**

### 5.2 API 能力分組

兩個新 group，前綴不與既有 `/api/v1/scout` 衝突。

| Group | Method | Path | 用途 |
|-------|--------|------|------|
| `radar` | GET / PUT | `/api/v1/radar/service-profile` | 讀／寫服務檔案（每會員一份） |
| `radar` | GET / POST | `/api/v1/radar/watches` | 列表／建立雷達訂閱 |
| `radar` | GET / PUT / DELETE | `/api/v1/radar/watches/:id` | 讀／改／封存 |
| `radar` | DELETE | `/api/v1/radar/watches/:id/purge` | 永久刪除已封存的 watch 設定（歷史資料保留） |
| `radar` | POST | `/api/v1/radar/watches/:id/pause`、`/resume` | 暫停／恢復 |
| `radar` | POST | `/api/v1/radar/watches/:id/sweep` | 手動觸發即時巡 → 回 Job |
| `radar` | POST | `/api/v1/radar/watches/suggest` | 依服務檔案建議關鍵字 |
| `radar` | GET | `/api/v1/radar/today` | 今日統計＋分級筆數＋卡片首頁 |
| `radar` | GET | `/api/v1/radar/opportunities` | 列表（filter：band／status／watch／日期） |
| `radar` | GET | `/api/v1/radar/opportunities/:id` | 詳情（含 `reasons[]`、變體、來源） |
| `radar` | POST | `/api/v1/radar/opportunities/:id/accept` | 加入名單 → 綁定／建立 Contact |
| `radar` | POST | `/api/v1/radar/opportunities/:id/dismiss` | 略過 |
| `radar` | POST | `/api/v1/radar/opportunities/:id/override` | 覆寫 band 或把 rejected 拉回 |
| `radar` | POST | `/api/v1/radar/opportunities/:id/replies` | 生成指定 variant 回覆 |
| `radar` | GET | `/api/v1/radar/sweeps` | 巡的執行紀錄（可觀測性） |
| `radar` | POST | `/api/v1/radar/import` | **P1** 手動匯入（網址／CSV） |
| `radar` | POST | `/api/v1/radar/explore` | **立即探索**：短詞 fan-out 搜尋 → 同一套五問判定 → 寫入今日商機（可選無 watch） |
| `crm` | GET | `/api/v1/crm/contacts` | 名單列表（query 名稱／帳號；filter：stage／follow_up／band／排序；分頁） |
| `crm` | GET | `/api/v1/crm/contacts/:id` | 詳情＋時間軸 |
| `crm` | DELETE | `/api/v1/crm/contacts/:id` | 從工作名單移除並關閉提醒；商機、接觸與成交歷史保留 |
| `crm` | POST | `/api/v1/crm/contacts/:id/stage` | 推進／回退階段 |
| `crm` | POST | `/api/v1/crm/contacts/:id/follow-up` | 設定／解除待追蹤標記與天數 |
| `crm` | POST | `/api/v1/crm/contacts/:id/notes` | 新增備註 |
| `crm` | POST | `/api/v1/crm/contacts/:id/merge` | 手動合併聯絡人 |
| `crm` | POST | `/api/v1/crm/contacts/:id/conversion` | 成交回報（寫既有 OutcomeEvent） |
| `crm` | PUT / DELETE | `/api/v1/crm/contacts/:id/conversion` | 改／刪成交（留痕） |
| `crm` | GET | `/api/v1/crm/followups` | 待追蹤清單 |
| `crm` | POST | `/api/v1/crm/followups/:id/done`、`/snooze` | 完成／延後 |
| `crm` | GET | `/api/v1/crm/stats` | 轉換統計（關鍵字／回覆版本／來源） |
| `scout`（既有擴充） | POST | `/api/v1/scout/posts/:id/promote` | 既有海巡命中升級為商機 |

### 5.3 前端頁面

| 路由 | 變更 |
|------|------|
| `/app/radar` | **新增** 今日商機：統計列＋卡片＋分級分組＋空狀態原因 |
| `/app/radar/today` | 今日商機：統計＋卡片；工具列含**立即探索**、手動匯入、訂閱管理、名單 |
| `/app/radar/watches` | **新增** 雷達訂閱管理＋關鍵字建議 |
| `/app/scout` | 話題靈感（activity only）；找需求請用商機頁 |
| `/app/crm` | **新增** 名單工作區：搜尋、階段／待追蹤篩選、排序、分頁；同頁詳情可推進、成交、備註與移除 |
| `/app/crm/:contactId` | 聯絡人詳情深連結（實際由 `/app/crm?contact=:id` 同頁工作區承載） |
| `/app/crm/stats` | **新增** 轉換統計（或內嵌 `/app/crm` 分頁） |
| `/app/brands` | 品牌與產品資料；舊 `?tab=serviceProfile` 自動轉址到 `/app/policy` |
| `/app/policy` | **獨立**商機與回覆 Policy：服務、價格、地區、禁語、案例、FAQ、空檔與口吻 |
| `/app/scout` 命中列 | ＋「升級為商機」動作 |
| `/app/today` | ＋今日商機摘要卡（連向 `/app/radar`） |
| 側欄 `nav.ts` | ＋新分組「商機」含 `radar`、`crm` 兩項；手機底欄以 `radar` 取代 `scout` 進主要四格，`scout` 移入「更多」 |

- 沿用既有設計系統：`src/components/ui/`、`src/styles/tokens.css`，不引入新 UI 框架、不用 emoji 當主 icon。
- 新頁樣式**不再灌入已 6,171 行的 `global.css`**：新增 `src/styles/radar.css` 並於 `main.tsx` 匯入，須通過 `npm run check:tokens`。
- 新增 repository 介面於 `src/data/repos.ts`（`radar`、`crm`），live 實作分檔 `src/data/live/radarRepos.ts`。

### 5.4 Job / Worker

| Template | 觸發 | 成功結果 |
|----------|------|----------|
| `radar_sweep` | 每日 UTC 22:00 排程（每個 active watch 一筆）／手動觸發 | 抓取→判定→寫 Opportunity；寫 RadarSweep 統計（命中、判定、截斷、失敗原因） |
| `radar_followup_scan` | 日排程（維護 tick，Redis lock） | 到期 FollowUp → `notified` ＋站內通知 |
| （既有）`scout_scan` | 不變 | 不變；**不得**改動既有海巡行為 |

- 沿用既有 Job guarded 狀態更新與 Redis lock（lock value = `workerID`）。
- `radar_sweep` 失敗須寫可讀錯誤原因並發通知，不得靜默。

### 5.5 計費對照（不新增 meter）

既有 `soft_caps` 各分項加總必須等於 `monthly_credits`，**新增 meter 會迫使三個方案全部重算**。因此本 run **沿用既有四個 meter**，僅以 `source` 標籤區分來源。

| 行為 | Meter | `source` |
|------|-------|----------|
| 巡的抓取（API 路徑） | `web_search` | `radar.sweep` |
| 五問判定 | `ai_research` | `radar.judge` |
| 回覆生成（各 variant） | `ai_copy` | `radar.reply` |
| AI 追蹤訊息 | `ai_copy` | `radar.followup` |
| 關鍵字建議 | `ai_copy` | `radar.suggest` |

- BYOK 規則不變；平台 gate 行為不變。
- 用量頁可依 `source` 前綴 `radar.` 聚出「雷達花了多少」，供 P1 價格校準使用。

### 5.6 錯誤語意

| 情境 | 行為 |
|------|------|
| 未建服務檔案就建 active watch | 明確錯誤，非 102000；message 指向服務檔案 |
| watch 數超過方案上限 | 明確錯誤；message 含目前上限與升級提示 |
| 當日商機達上限 | **不是錯誤**：正常回應並帶 `truncated_count` 與提示文案 |
| 回覆生成兩次都命中 `forbidden[]` | 明確錯誤；不得輸出違規內容 |
| AccountHealth `throttle` 時嘗試自動送出 | 沿用既有錯誤語意；僅留手動路徑 |
| 抓取路徑不可用 | Sweep `failed` ＋通知；**禁止**空成功 |
| 合併聯絡人跨不同 owner | 明確錯誤（越權） |
| 成交回報金額為負或幣別非法 | 明確錯誤 |

## 6. 與現況對照（Retain / Replace / Remove）

| 能力 | Retain | Replace | Remove | 備註 |
|------|--------|---------|--------|------|
| 海巡三模式（product／theme／activity） | ✅ 原樣 | — | — | 不改行為 |
| 海巡 Brand／Product／pain_points | ✅ | — | — | 與 ServiceProfile 並存，非取代 |
| 海巡抓取雙路徑 | ✅ 復用 | — | — | 不新增第三種抓取 |
| 既有貼文分類（`seeking_help` 等） | ✅ 復用為第一問依據 | — | — | 不重做分類器 |
| ScoutPost `outreach_status` | ✅ | — | — | 與 Opportunity 狀態**不共用** |
| 海巡命中列表 | ✅ | **＋**「升級為商機」 | — | 複製＋反向連結 |
| OutcomeEvent 歸因 | ✅ | **＋** `source_type=radar_opportunity` | 另建成交帳 | |
| AccountHealth 送出閘 | ✅ 強制經過 | — | 繞過護欄的新路徑 | |
| Outbox 真發送 | ✅ | — | — | 公開留言版沿用 |
| Usage meter 四分項 | ✅ | **＋** `source` 標籤 | 新增第五個 meter | 避免重算 soft_caps |
| 付費流程（Stripe 訂閱／結帳／portal／webhook） | ✅ 原樣 | **＋**配額欄位 | 重做付費流程 | 金流已存在 |
| 方案價格 Free／Starter／Pro | ✅ 不改價 | — | — | 校準列 P1 |
| 站內通知 | ✅ | **＋** `ref_type=contact` | — | |
| 前端設計系統與 token 守門 | ✅ | **＋** `radar.css` | 樣式灌 `global.css` | |
| `lead` 指稱銷售線索 | — | — | **禁止** | 既有語意為主帳號 |
| 模擬／空成功 API | — | — | **禁止** | AGENTS.md |

## 7. 非功能

- **安全／權限：** 全部新資源以 `owner_uid` 隔離；Contact 合併需同 owner；Admin 不預設讀全站商機（如需另開需求）。
- **隱私：** 只處理公開貼文與使用者主動匯入內容；Contact 僅存公開作者識別與使用者自填備註，**不做跨平台身分推測**。
- **合規：** 抓取僅走既有雙路徑；`dev_mode` 爬蟲路徑不擴大；所有自動化保留「複製 → 手動送出 → 標記完成」人工路徑。
- **可觀測性：** RadarSweep 記錄每次巡的命中／判定／截斷／失敗原因與耗用點數；FollowUp 掃描寫日誌；日誌不含 token 明文。
- **效能邊界：** 列表 `pageSize ≤ 50`；單次 sweep 判定筆數以方案每日上限為硬上限；判定為逐筆 AI 呼叫，須可中斷並記錄已完成進度（Job 失敗不重跑已判定者）。
- **成本：** 每日巡是常駐成本，必須有 §4.9 配額閘；`radar.` source 聚合是價格校準的資料來源。
- **語系：** 判定理由與回覆文案預設繁體中文台灣語感。

## 8. 資料保存（邏輯層）

| 實體 | 關鍵欄位 | 生命週期 |
|------|----------|----------|
| `radar_service_profiles` | `_id=owner_uid`, services[], cases[], forbidden[], faq[], service_areas[], remote_ok, availability, tone_note, updated_at | 每會員一份，可改 |
| `radar_watches` | id, owner_uid, terms[], exclude_terms[], regions[], status, last_swept_at, created_at | CRUD＋封存；使用者確認後可刪除 archived 設定，歷史紀錄保留 |
| `radar_sweeps` | id, owner_uid, watch_id, job_id, path(api/crawler), hit_count, judged_count, created_count, truncated_count, failed_reason?, credits_used, started_at, ended_at | 保留供觀測與成本分析 |
| `radar_opportunities` | id, owner_uid, watch_id?, source(threads/manual/scout_promote), source_scout_post_id?, external_id, permalink, author_handle, text, posted_at, status, intent_score, intent_band, reasons[], region_detected, region_match, freshness_hours, matched_service, reject_reason?, override?, contact_id?, created_at | 判定後入庫；rejected 亦留 |
| `radar_replies` | id, owner_uid, opportunity_id, variant, text, used_at?, sent_channel?, created_at | 按需生成 |
| `crm_contacts` | id, owner_uid, source_platform, author_handle, display_name?, stage, needs_follow_up, follow_up_days, last_touch_at, opportunity_ids[], merged_from[], removed_at?, created_at | 唯一鍵（owner_uid, source_platform, author_handle）；移除後保留，重新加入時恢復 |
| `crm_touches` | id, owner_uid, contact_id, type(stage/reply/note/conversion), from_stage?, to_stage?, body?, actor_uid, created_at | 只增不改；成交修改寫新一筆 |
| `crm_followups` | id, owner_uid, contact_id, due_at, status, notified_count, created_at | 到期→通知→終態 |
| 成交 | **不新增 collection**，寫既有 `growth_outcomes` | `source_type=radar_opportunity` |

- 索引：`owner_uid` 為所有查詢前綴；`radar_opportunities` 需 `(owner_uid, created_at)`、`(owner_uid, intent_band, status)`、`(owner_uid, external_id)` 去重；`crm_contacts` 需唯一鍵；`crm_followups` 需 `(status, due_at)`。
- 去重：同一 `owner_uid` 下同 `external_id` 不重複建立 Opportunity（跨 watch 命中同一貼文只留一筆，記錄多個觸發 term）。

## 9. 驗收場景（Given / When / Then）

**共通：** 成功 envelope `102000`；時間 ns UTC；未實作 ≠ 空成功。

### 9.1 服務檔案與雷達訂閱

| ID | Given | When | Then |
|----|-------|------|------|
| SP-01 | 新會員無服務檔案 | 建 active watch | 明確錯誤；指向服務檔案 |
| SP-02 | 已填服務檔案 | GET service-profile | 欄位完整回傳；`forbidden[]` 可讀 |
| RW-01 | Free 方案已有 1 個 active watch | 再建一個 | 明確錯誤含上限與升級提示 |
| RW-02 | active watch | pause | 不再排入每日巡；既有商機保留 |
| RW-03 | 有服務檔案 | watches/suggest | 回關鍵字建議清單，可逐條採用 |
| RW-04 | archived watch | 列表 | 不再產生新商機；歷史商機與統計不變 |
| RW-05 | archived watch | 確認永久刪除 | watch 設定不再出現在列表；sweeps、opportunities 與統計仍可查 |

### 9.2 每日巡與判定

| ID | Given | When | Then |
|----|-------|------|------|
| SW-01 | 有 2 個 active watch | 排程到 UTC 22:00 | 各建一筆 `radar_sweep` Job；隔日台北早晨可見結果 |
| SW-02 | `dev_mode_enabled=false` | sweep | 走 API 路徑；`path=api` |
| SW-03 | `dev_mode_enabled=true` 且有工作階段 | sweep | 走爬蟲路徑；`path=crawler` |
| SW-04 | 抓取路徑不可用 | sweep | Sweep `failed` ＋原因＋通知；**非**空成功 |
| SW-05 | 命中 40 筆、方案上限 30 | sweep | 依 score 保留 30；`truncated_count=10`；今日頁提示 |
| SW-06 | 同一貼文被兩個 watch 命中 | sweep | 只建 1 筆 Opportunity，記錄兩個觸發 term |
| OP-01 | 貼文為「求推薦台北婚攝」、服務地區台北 | 判定 | `qualified`；`region_match=match`；`reasons[]` 五條齊全 |
| OP-02 | 貼文為同業叫賣（`provider_offer`） | 判定 | `rejected`；不出現在今日頁；仍可查 |
| OP-03 | 貼文發布 20 天前 | 判定 | 硬否決 `rejected`（逾期） |
| OP-04 | 貼文未提地區 | 判定 | `region_match=unknown`；得部分分；**不猜測縣市** |
| OP-05 | 服務檔案標可遠端、貼文在高雄、服務地區台北 | 判定 | 不因地區硬否決；地區維度給滿分 |
| OP-06 | 任一維度缺理由 | 寫入 | **拒絕入庫**（契約要求 `reasons[]` 五條齊全） |
| OP-07 | 使用者覆寫 band | override | 生效並記錄原值／新值／時間／操作者 |

### 9.3 今日頁與回覆

| ID | Given | When | Then |
|----|-------|------|------|
| TD-01 | 當日有 18 筆 | GET today | 統計為 18 與高／中／低分項；卡片含原文、分數、地區、發布時間、預設回覆 |
| TD-02 | 當日 0 筆 | GET today | **非空白**：回明確原因與下一步建議 |
| RP-01 | 商機剛判定完 | 查 replies | 僅存在 `public_comment` 一個版本 |
| RP-02 | 使用者要私訊版 | 生成 `dm` | 產出並計入 `ai_copy`／`source=radar.reply` |
| RP-03 | 服務檔案 forbidden 含「保證錄取」 | 生成回覆 | 輸出不含該詞；連兩次命中則明確錯誤且不輸出 |
| RP-04 | 帳號 AccountHealth `throttle` | 自動送出公開留言 | **拒絕**；僅留複製＋標記完成 |
| RP-05 | 私訊版 | 送出 | P0 僅提供複製；不宣稱已自動送出 |

### 9.4 CRM 與追蹤

| ID | Given | When | Then |
|----|-------|------|------|
| CR-01 | 商機 `qualified` | accept | 建立或綁定 Contact；`stage=new_found` |
| CR-02 | 同一作者第二筆商機 | accept | 掛在**同一** Contact 下；時間軸兩筆 |
| CR-03 | Contact 在 `dm_sent` | 直接跳 `won` | 允許；寫 ContactTouch 記錄跳轉 |
| CR-04 | Contact `won` | 重新開啟為 `replied` | 允許；留痕 |
| CR-05 | 任一階段 | 標記待追蹤 | `needs_follow_up=true` 與階段**並存**，非取代 |
| CR-06 | 兩個不同平台的 Contact | merge | 允許手動合併；時間軸保留兩邊 |
| CR-07 | 跨 owner 的 Contact | merge | 明確錯誤（越權） |
| CR-08 | 回報成交金額 12000 | conversion | 階段轉 `won`；寫入既有 `growth_outcomes`（`source_type=radar_opportunity`）；成果頁可見 |
| CR-09 | 修改／刪除成交 | PUT／DELETE | 明細與成果彙總一致；留痕；非空成功 |
| CR-10 | Contact 有商機、接觸、成交與待追蹤 | DELETE Contact | 名單與提醒不再顯示；商機、接觸、成交歷史保留 |
| CR-11 | 已移除 Contact 的同一作者又有商機 | accept | 恢復原 Contact 並掛上新商機，不建立重複聯絡人 |
| FU-01 | 送出回覆後 3 天無動作 | followup scan | FollowUp `notified` ＋站內通知，落地 Contact 詳情 |
| FU-02 | 通知中 | 產生 AI 追蹤訊息 | 產出並計入 `ai_copy`／`source=radar.followup` |
| FU-03 | 已通知 2 次仍無動作 | scan | `escalated` ＋顯示「建議轉未成交」；**不自動改階段** |
| FU-04 | 使用者延後 5 天 | snooze | `due_at` 後移；回 `scheduled` |

### 9.5 統計與配額

| ID | Given | When | Then |
|----|-------|------|------|
| ST-01 | 某關鍵字 accepted 8、won 2 | GET stats | 顯示轉換率與絕對數 |
| ST-02 | 某關鍵字樣本 3 筆 | GET stats | 只顯示絕對數並標「樣本不足」，不顯示排名結論 |
| ST-03 | 有 radar／scout／manual 三種來源成交 | GET stats | 依來源分列 |
| QT-01 | 當日已達商機上限 | 再次 sweep | 不新增；回應帶 `truncated_count` 與提示；**非錯誤** |
| QT-02 | 平台點數耗盡（非 BYOK） | 判定 | 沿用既有 platform gate 行為；watch **不被刪除** |
| QT-03 | 用量頁 | 依 source 聚合 | 可看出 `radar.*` 的耗用，供價格校準 |

### 9.6 既有海巡橋接與回歸

| ID | Given | When | Then |
|----|-------|------|------|
| PR-01 | 既有海巡命中一筆 | promote | 建立**新** Opportunity 並記 `source_scout_post_id`；原 ScoutPost 狀態不變 |
| PR-02 | promote 後推進 CRM | 操作 | 不影響原 `outreach_status` |
| RG-01 | 既有海巡三模式 | 執行 brief／scan | 行為與現況完全一致 |
| RG-02 | 既有 Outbox 真發送 | 送出 | 仍僅平台成功後才 published |
| RG-03 | 既有 growth-loop 成果卡 | 開啟 | 既有歸因不受影響，radar 成交併入同一彙總 |
| RG-04 | 既有 Usage soft_caps | 檢查 | 四分項加總仍等於 `monthly_credits`（未新增 meter） |
| RG-05 | 全 repo | 檢索 | 無以 `lead` 指稱銷售線索的新程式碼或契約 |

## 10. 開放規格問題

1. ~~巡的觸發時段~~ → **已定：每日 UTC 22:00（台北 06:00）**；個人化時區 P2（§4.2）。
2. ~~既有海巡命中升級是複製或共用~~ → **已定：複製＋反向連結**（§4.10）。
3. ~~是否新增 usage meter~~ → **已定：不新增**，以 `source` 標籤區分（§5.5）。
4. 意向分數五維度權重數值 → **plan／實作可調**；**band 門檻 80／50 與硬否決條件鎖定**。
5. 地區判定的縣市代碼表來源與比對規則細節 → **plan**（行為已鎖：三態、不猜測）。
6. 側欄新增兩項後共 13 項是否需要再收斂資訊架構 → **plan 決定**；本 spec 已先以分組與手機底欄調整緩解。
7. 關鍵字建議的產生方式（純 AI 或 AI＋既有痛點關鍵字工具）→ **plan**。

## 11. 批准

- [x] Spec 已批准（2026-07-31／使用者「好」）
