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

476 lines
37 KiB
Markdown
Raw Permalink 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-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`、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 --> (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 5079 | low < 50
```
- 每個維度必須輸出**分數與人話理由**`reasons[]` 缺任一維度不得入庫。
- 地區三態:`match``mismatch``unknown`。`unknown` 得部分分(不等同 `mismatch`),且**禁止猜測縣市**。ServiceProfile 標記可遠端服務時,`mismatch` 不觸發硬否決且地區維度給滿分。
- 時效:距今 ≤ 24h 給滿分2472h 遞減72h14 天低分,> 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 加入名單與 CRMP0
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 三模式與 BrandProduct 基礎設施仍保留
### 4.12 立即探索與 Threads 短詞2026-08-07
1. **單一找需求管線:** 每日 watch 立即探索手動匯入皆經同一套五問判定進今日商機
2. **`POST /api/v1/radar/explore`** req `{ terms: string[] }`16 每組必須通過 Threads 短詞規則不合規整組擋下不默默修正同步執行 term fan-out 搜尋去重)→ 分類 `ProcessCandidates`每日配額去重)→ `RadarSweep``watch_id` 可空)→ `hit_count``judged_count``created_count``truncated_count``credits_used`。
3. **Threads 短詞規則:** 一組 2 token半形空格)、中文每詞 24 整組去掉空格 12 禁標點booleanemoji#。建議關鍵字explore話題工坊共用此規則訂閱表單對不合規詞顯示警示不硬擋既有資料)。
4. **每日巡 fan-out** `SearchHitsOnly` term 搜尋再去重禁止把整組 terms join 成一條 query稀釋召回)。
5. **話題今日目標補抓:** 話題頁`/app/scout`設定今日目標後掃描 brief `target_count`剩餘則數上限 40)。`RunScanFromBrief`主路徑 fan-out 後若 hit < target先同路徑加碼再搜若主路徑為 crawler 仍不足再以 search providerExa domain-restricted只補缺口並 dedupe不湊數無限重試仍不足則以實際命中數入庫並讓 UI 顯示
6. **話題自然語意:** activity 話題工坊接受自然輸入並保留整體意圖工作方向組成職種外包接案推薦」,作品人物事件保留原名並補熱門最新查詢前端預選排序最前的 3 fan-out含多個主題核的 query 必須在正文同時命中各核心熱門最新等 discovery 修飾詞不要求正文逐字出現crawler 同時命中 toprecent`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 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` | 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` | 列表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 |
| `radar` | POST | `/api/v1/radar/explore` | **立即探索**:短詞 fan-out 搜尋 → 同一套五問判定 → 寫入今日商機(可選無 watch |
| `crm` | GET | `/api/v1/crm/contacts` | 名單列表query 名稱帳號filterstagefollow_upband排序分頁 |
| `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` | 日排程(維護 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封存使用者確認後可刪除 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 | 修改/刪除成交 | PUTDELETE | 明細與成果彙總一致;留痕;非空成功 |
| 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 | 有 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使用者「好」