thread-master/docs/product/demand-radar/spec.md

453 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Spec: 商機雷達+成交 CRM
> Status: `approved`
> Status note: 使用者 2026-07-31「好」spec 通過)
> Source: `docs/product/demand-radar/requirements.md`**approved** 2026-07-31
> Last updated: `2026-07-31`
> 底座:`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`、BrandProduct、OutboxBundle/Step、Persona、Usage/PlanFreeStarterPro、Job、Notification、OutcomeEventAttributionLink、AccountHealth。
**命名紅線:** 全域禁用 `lead` 指稱銷售線索(既有語意為 ThreadPlay 主帳號)。
## 3. 狀態機
### 3.1 RadarWatch
```text
active --> paused : 使用者暫停(不再排入每日巡)
paused --> active : 恢復
active|paused --> archived : 軟刪;歷史商機保留
```
- `archived` 的 watch 不再產生新商機,但既有商機與統計**不刪除**。
- 超過方案 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 5079 | low < 50
```
- 每個維度必須輸出**分數與人話理由**`reasons[]` 缺任一維度不得入庫。
- 地區三態:`match``mismatch``unknown`。`unknown` 得部分分(不等同 `mismatch`),且**禁止猜測縣市**。ServiceProfile 標記可遠端服務時,`mismatch` 不觸發硬否決且地區維度給滿分。
- 時效:距今 ≤ 24h 給滿分2472h 遞減72h14 天低分,> 14 天硬否決。
### 3.4 Contact CRM 階段
七個互斥階段 一個並存標記畫面上呈現為八格requirements 決策 #5)。
```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 加入名單與 CRMP0
1. 「加入名單」時依(`source_platform``author_handle``owner_uid`)尋找既有 Contact命中則掛上未命中則建立初始 `stage=new_found`
2. 同一人多筆商機**收斂在同一 Contact 下**,時間軸可看到全部接觸歷史。
3. **跨平台不自動合併**;提供手動 `merge``unmerge`,合併保留兩邊時間軸。
4. CRM 列表以 Contact 為單位,可依階段、`needs_follow_up`、意向分數、最後接觸時間篩選排序。
5. 階段推進、備註、標記待追蹤皆寫 `ContactTouch`
6. 成交回報:填金額(選填)與備註 → 階段轉 `won`**寫入既有 OutcomeEvent**`source_type=radar_opportunity`,連 `opportunity_id`、`contact_id`),不另建成交帳。可事後修改/刪除並留痕。
### 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. 既有海巡命中列表新增動作升級為商機」。
2. **決策:複製,不共用**requirements 開放問題 #5)。建立新的 Opportunity 並記 `source_scout_post_id` 反向連結兩邊狀態機互不影響避免 `outreach_status` Opportunity 狀態互相污染
3. 升級後跑同一套五問判定進同一條 CRM
4. 既有海巡三模式`product``theme``activity`與品牌產品模型**原樣保留**不修改其行為
### 4.11 P1P2 邊界
| 能力 | 波次 | 行為邊界 |
|------|------|----------|
| 手動匯入 | P1 | ThreadsFacebook 貼文網址或 CSV Opportunity 跑同一套判定來源標 `manual_import` |
| 報價與成交金額 | P1 | `quoted``won` 可填金額幣別預設 TWD寫既有 OutcomeEvent |
| 價格帶校準 | P1 | P0 每會員雷達成本數據決定是否調 StarterPro 價格 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` 數字
- AuthBearer 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` | 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` | 列表filterbandstatuswatch日期 |
| `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 |
| `crm` | GET | `/api/v1/crm/contacts` | 名單列表filterstagefollow_upband排序 |
| `crm` | GET | `/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/watches` | **新增** 雷達訂閱管理關鍵字建議 |
| `/app/crm` | **新增** 名單八格視圖七階段待追蹤)、篩選排序 |
| `/app/crm/:contactId` | **新增** 聯絡人詳情時間軸成交回報 |
| `/app/crm/stats` | **新增** 轉換統計或內嵌 `/app/crm` 分頁 |
| `/app/brands` | +「服務檔案分頁避免側欄再膨脹 |
| `/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` | 日排程維護 tickRedis lock | 到期 FollowUp `notified` 站內通知 |
| 既有`scout_scan` | 不變 | 不變**不得**改動既有海巡行為 |
- 沿用既有 Job guarded 狀態更新與 Redis locklock 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 | 明確錯誤 102000message 指向服務檔案 |
| watch 數超過方案上限 | 明確錯誤message 含目前上限與升級提示 |
| 當日商機達上限 | **不是錯誤**正常回應並帶 `truncated_count` 與提示文案 |
| 回覆生成兩次都命中 `forbidden[]` | 明確錯誤不得輸出違規內容 |
| AccountHealth `throttle` 時嘗試自動送出 | 沿用既有錯誤語意僅留手動路徑 |
| 抓取路徑不可用 | Sweep `failed` 通知**禁止**空成功 |
| 合併聯絡人跨不同 owner | 明確錯誤越權 |
| 成交回報金額為負或幣別非法 | 明確錯誤 |
## 6. 與現況對照Retain / Replace / Remove
| 能力 | Retain | Replace | Remove | 備註 |
|------|--------|---------|--------|------|
| 海巡三模式productthemeactivity | 原樣 | | | 不改行為 |
| 海巡 BrandProductpain_points | | | | ServiceProfile 並存非取代 |
| 海巡抓取雙路徑 | 復用 | | | 不新增第三種抓取 |
| 既有貼文分類`seeking_help` | 復用為第一問依據 | | | 不重做分類器 |
| ScoutPost `outreach_status` | | | | Opportunity 狀態**不共用** |
| 海巡命中列表 | | ****「升級為商機 | | 複製反向連結 |
| OutcomeEvent 歸因 | | **** `source_type=radar_opportunity` | 另建成交帳 | |
| AccountHealth 送出閘 | 強制經過 | | 繞過護欄的新路徑 | |
| Outbox 真發送 | | | | 公開留言版沿用 |
| Usage meter 四分項 | | **** `source` 標籤 | 新增第五個 meter | 避免重算 soft_caps |
| 付費流程Stripe 訂閱結帳portalwebhook | 原樣 | ****配額欄位 | 重做付費流程 | 金流已存在 |
| 方案價格 FreeStarterPro | 不改價 | | | 校準列 P1 |
| 站內通知 | | **** `ref_type=contact` | | |
| 前端設計系統與 token 守門 | | **** `radar.css` | 樣式灌 `global.css` | |
| `lead` 指稱銷售線索 | | | **禁止** | 既有語意為主帳號 |
| 模擬空成功 API | | | **禁止** | AGENTS.md |
## 7. 非功能
- **安全權限** 全部新資源以 `owner_uid` 隔離Contact 合併需同 ownerAdmin 不預設讀全站商機如需另開需求)。
- **隱私** 只處理公開貼文與使用者主動匯入內容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封存 |
| `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[], 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 | 列表 | 不再產生新商機歷史商機與統計不變 |
### 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 | 修改刪除成交 | PUTDELETE | 明細與成果彙總一致留痕非空成功 |
| 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 8won 2 | GET stats | 顯示轉換率與絕對數 |
| ST-02 | 某關鍵字樣本 3 | GET stats | 只顯示絕對數並標樣本不足」,不顯示排名結論 |
| ST-03 | radarscoutmanual 三種來源成交 | 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 | 既有海巡三模式 | 執行 briefscan | 行為與現況完全一致 |
| 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 門檻 8050 與硬否決條件鎖定**。
5. 地區判定的縣市代碼表來源與比對規則細節 **plan**行為已鎖三態不猜測)。
6. 側欄新增兩項後共 13 項是否需要再收斂資訊架構 **plan 決定** spec 已先以分組與手機底欄調整緩解
7. 關鍵字建議的產生方式 AI AI既有痛點關鍵字工具)→ **plan**
## 11. 批准
- [x] Spec 已批准2026-07-31使用者」)