# Spec: 商機工作收件匣與產品需求搜尋 > Status: `approved` > Status note: 使用者 2026-08-11「批准」 > Source: `docs/product/opportunity-inbox/requirements.md`(approved 2026-08-11) > Parent specs: `docs/product/demand-radar/spec.md`、`docs/product/brand-product-radar/spec.md` > Last updated: `2026-08-12` ## 1. 摘要 本規格把現有「今日商機」與「全部結果」合併成一個商機工作收件匣,以獨立的 `review_state` 管理待處理、已完成與已移除,不改變既有加入 CRM、判定合格或拒絕等業務狀態。移除是可復原的軟移除,且保留來源 tombstone,確保下一次巡邏不會把同一內容重新建立成新商機。 搜尋改為「產品真相 → 需求地圖 → 多組使用者語言 → 去重與前處理 → 輕量排序 → AI 深度判定」。預設推薦分數以產品痛點適配為主,另支援來源發布時間等排序。所有付費動作都沿用既有 meter、平台代付與 BYOK 規則,加入執行前預估、每次巡邏成本上限、階段用量明細,以及同候選同產品輸入版本不重複判定與扣點的契約。 ## 2. 名詞與實體 | 名詞 | 定義 | |------|------| | **OpportunityStatus** | 既有業務/判定狀態:`judging / qualified / rejected / accepted / dismissed`;`accepted` 仍表示已加入 CRM | | **ReviewState** | 新增的工作收件匣狀態:`pending / completed / removed`,與 OpportunityStatus 分開 | | **Soft removal** | 將 ReviewState 設為 `removed` 並保存原因、時間與操作者;資料與來源識別仍保留 | | **Source tombstone** | 已移除商機保留的 owner+來源唯一識別;後續命中只合併來源,不建立新卡、不自動恢復 | | **DemandMap** | 一個產品可檢查的需求地圖:困擾語言、情境、期望結果、解法意圖與排除訊號 | | **DemandInputVersion** | 由受眾、痛點、情境、能力、匹配與排除內容決定的產品搜尋輸入版本;純名稱或顯示欄位修改不改版 | | **DemandMapVersion** | 需求地圖每次有效儲存的版本,供搜尋計畫與判定防重使用 | | **QueryPlan** | 從目前 DemandMap 產生、符合來源短詞限制的多組搜尋語言與排除語言 | | **CandidateDisposition** | 前處理結果:`pass / review / reject`;reject 不進 AI 深判,review 可低順位保留或抽樣深判 | | **DemandEvidence** | 候選原文中表達問題、求助、求推薦、比較或想達成結果的短片段 | | **PriorityScore** | `priority_score`,0–100;決定收件匣推薦順序,與既有 `intent_score`、`product_fit_score` 分開 | | **Billable action** | 會使用平台代付搜尋或 AI provider 的動作;純 UI、規則前處理與已保存結果讀取不是付費動作 | | **CostPreview** | 付費動作執行前的點數估計,含固定最低值、可能最高值、BYOK 模式及估計依據 | | **CreditCeiling** | 單次手動工作或每日自動巡邏最多可新增消耗的平台點數;不得超額後再通知 | ## 3. 狀態機與不變量 ### 3.1 工作收件匣狀態 ```text pending --> completed : 使用者標示完成,或成功加入 CRM completed --> pending : 使用者恢復待處理 pending --> removed : 使用者移除並選原因 completed --> removed : 使用者移除並選原因 removed --> pending|completed : 復原到移除前狀態 ``` - 每筆 Opportunity 恰有一個 ReviewState。 - `completed` 只代表使用者已處理,不代表適合/成交/加入 CRM,不形成負向訓練訊號。 - `removed` 必須保存 `removal_reason`;`other` 必須另有非空白說明。原因 enum:`pain_mismatch / provider_or_ad / stale / already_solved / duplicate / other / legacy_unknown`。 - 進入 `removed` 時保存 `previous_review_state`、`removed_at`、`removed_by`;復原回到 `previous_review_state`,缺值時回 `pending`,並保存最近一次移除紀錄供稽核。 - 更新成目前相同狀態是 idempotent success,不重複寫操作、不產生用量。 - ReviewState 不得直接改寫 OpportunityStatus。既有 `accept` 成功後仍設 `accepted`、綁 Contact,另外把 ReviewState 設為 `completed`。 - 舊 `dismiss` 行為保留相容,但改為設定 OpportunityStatus=`dismissed` 且 ReviewState=`removed`;reason 空白的舊呼叫保存 `legacy_unknown`。 ### 3.2 舊資料讀取規則 缺 `review_state` 的既有 Opportunity 依下列順序讀取: ```text status=accepted -> completed status=dismissed -> removed(removal_reason=legacy_unknown) 其他 -> pending ``` - 第一次狀態寫入時持久化映射結果;列表讀取不可要求一次性全庫遷移才可用。 - `dismissed` 舊資料復原時只恢復 ReviewState;既有 OpportunityStatus 保持 `dismissed`,避免竄改歷史業務狀態。若需要重新成為 qualified,仍走既有人工 override。 ### 3.3 去重與 tombstone - 沿用「同 owner+同來源內容識別只一筆 Opportunity」。來源識別優先用正規化 external ID,其次正規化 permalink。 - 查到既有 `removed` 商機時,只合併新的 watch/term/產品匹配來源與最近命中時間;不得建立新 Opportunity、不得自動改回 pending、不得重複深判。 - `duplicate` 移除原因可保存 `duplicate_of_opportunity_id`;被指向的主卡不存在或不屬於同 owner 時拒絕操作。 - tombstone 沿用既有資料保留政策;P0 不提供不可逆硬刪除 API。 ## 4. 搜尋、前處理與排名契約 ### 4.1 DemandMap 結構 每個產品目前版本只有一份生效 DemandMap: ```text DemandMap = { product_id, demand_input_version, map_version, state, pain_phrases[], scenario_phrases[], desired_outcomes[], solution_signals[], exclusion_signals[], source_basis[], custom_phrases[], ai_enriched_at?, updated_at } ``` - `state` 為 `ready / incomplete / stale`。 - 每個 phrase 保存 `text`、`kind`、`basis_kind`、`basis_text`、`origin=product|user|ai`、`enabled`;不得只保存無法追溯來源的模型輸出。 - 系統從 Brand/Product 真相建立免費 baseline;不得為了開頁或每日巡邏自動呼叫 AI。 - 使用者手動編輯與啟停 phrase 不扣點,每次有效儲存增加 map_version。 - AI enrichment 是明確的手動付費動作;成功後新增 AI phrases 並保存來源依據,不覆寫使用者自訂內容。 - 產品受眾、痛點、情境、能力、匹配或排除內容實質變更時,DemandInputVersion 改變;舊 AI enrichment 失效,免費 baseline 以新產品真相重建,使用者自訂內容保留但標示待確認。 - 純品牌/產品名稱、圖片或顯示順序修改不改 DemandInputVersion,也不允許重新扣同候選深判點數。 - baseline 缺少痛點且缺少情境/期望結果時為 `incomplete`;可保存但不能啟動新的產品巡邏,畫面引導補產品資料。 ### 4.2 QueryPlan 1. 只從 enabled DemandMap phrases 產生符合 Threads 短詞契約的 1–6 組查詢;每組至少包含困擾/情境/期望結果之一,產品名不得單獨成組。 2. solution signals 用於提升求助、求推薦、比較與購買意圖召回;exclusion signals 傳給既有排除流程。 3. 同義表達分散在不同查詢組,結果在 owner 層統一去重;增加 query 組數不得繞過單次 CostPreview 與 CreditCeiling。 4. 每次 Sweep 保存實際使用的 demand_input_version、map_version 與 query plan 摘要,使結果可重現。 5. 使用者手動修改 watch terms 時仍可使用,但 UI 顯示它們與 DemandMap 的關聯;純產品名 term 只能輔助召回,不能提供排名證據。 ### 4.3 候選前處理 候選依序經過下列不呼叫 AI provider 的階段;每階段保存計數與 machine reason: 1. **正規化與去重:** 正規化 external ID/permalink,同一來源只保留資訊最完整的一筆。 2. **基本有效性:** 必須有可用文字與來源;語言不支援、時間未知不直接 reject,但留下風險。 3. **硬排除:** 命中產品排除訊號,或明確為同業供給、廣告、品牌公告、抽獎/互追雜訊時 disposition=`reject`。 4. **證據抽取:** 從原文擷取 DemandEvidence;找不到任何問題、求助、比較、求推薦或期望結果訊號時 disposition 至少降為 `review`。 5. **產品證據:** 用 DemandMap 對困擾、情境、期望結果做輕量比對;產品名/品牌名命中不計分。 6. **分流:** 同時具備 DemandEvidence 與至少一項產品證據為 `pass`;只有其中一項為 `review`;兩者皆無為 `reject`。 - reject 候選不進 AI 深度判定,也不建立高/中/低商機;Sweep 保留原因計數,不保存作者敏感資訊的額外副本。 - review 候選可按輕量分數進入低順位結果或在 CreditCeiling 內抽樣深判;不得標為高順位。 - 已存在且已有同 DemandInputVersion+DemandMapVersion 有效判定的候選直接復用,不再呼叫 AI。 ### 4.4 AI 深度判定 - 只對 `pass` 的高價值候選及部分 `review` 不確定候選執行,順序以輕量產品證據、需求意圖與新鮮度排列。 - 輸出必須包含結構化 DemandEvidence、產品適配證據、排除風險與各分數;所有正向重要理由都須引用候選原文片段及 DemandMap/產品依據。 - AI 不得用品牌名、產品名或搜尋詞本身補成痛點證據;不得推測作者未公開的身分、預算或敏感屬性。 - 結構不完整、證據無法在原文找到或產品依據不存在時,該判定失敗且不得保存為有效結果;工作可在剩餘 CreditCeiling 內繼續下一筆。 - 判定結果為不適合但 provider 已成功完成有效結構化工作,仍正常計點並保存拒絕原因,避免日後重判重扣。 ### 4.5 推薦分數與硬門檻 ```text priority_score = pain_fit(0..45) + demand_intent(0..25) + evidence_quality(0..15) + freshness(0..15) ``` | 因子 | 行為定義 | |------|----------| | `pain_fit` | 原文對 Product 痛點、情境或期望結果的適配;至少一項有可引用證據才可大於 0 | | `demand_intent` | 是否正在求助、求推薦、比較方案、表達想改善或準備採取行動 | | `evidence_quality` | 引文是否直接、完整且同時支持需求與產品適配;只有推測時為 0 | | `freshness` | 沿用來源 posted_at 時效衰減;未知時間為 0,不得視為最新 | ```text priority_band = high if score >= 75 and hard gates pass review if score 45..74 and hard gates pass low otherwise but has basic demand + product evidence ``` 高順位 hard gates: - 至少一段 DemandEvidence。 - 至少一項痛點、情境或期望結果的產品證據。 - 非 provider/廣告/公告/雜訊,且沒有產品硬排除。 - 不能只靠品牌名、產品名或 matched term。 - 來源與引文可回看;證據不足只能進 review/low。 排序 enum: | sort | 排序 | |------|------| | `recommended` | priority_score desc → product_fit_score desc → posted_at desc → id asc | | `newest` | 已知 posted_at desc → priority_score desc;未知時間置底 | | `oldest` | 已知 posted_at asc → priority_score desc;未知時間置底 | | `product_fit` | product_fit_score desc → priority_score desc → posted_at desc | | `demand_intent` | demand_intent desc → priority_score desc → posted_at desc | - 所有分頁排序必須穩定,最後以 id tie-break;禁止前端只對單頁資料重排。 - 舊 `sort=score` 相容映射為 `recommended`,舊 `sort=posted` 相容映射為 `newest`。 ## 5. 扣點與配額契約 ### 5.1 免費與付費邊界 | 動作 | 平台點數 | |------|----------| | 開頁、列表、詳情、篩選、排序 | 0 | | 完成、移除、復原、選移除原因 | 0 | | baseline DemandMap 建立、手動編輯、QueryPlan 規則產生 | 0 | | 去重、硬排除、證據抽取、輕量排序 | 0 | | API path 搜尋 | 沿用既有 `web_search` 單次成本與 `radar.*` source | | crawler path 搜尋 | 沿用既有 crawler/BYOK 計量行為,不臆造平台搜尋點數 | | DemandMap AI enrichment | 沿用既有 AI meter,以獨立 `radar.demand_map` source 歸因 | | AI 深度判定 | 沿用既有 AI research/判定 meter,以 `radar.judge` source 歸因 | | 回覆生成 | Retain 既有 meter 與按需生成規則 | - 不新增第五種 usage meter;`radar.*` source 只用於歸因。 - BYOK 的平台 credits 一律為 0,但 count、source、工作統計仍增加。 ### 5.2 CostPreview 與執行確認 CostPreview 回傳: ```text { action, key_mode, fixed_credits, min_credits, max_credits, search_calls, max_ai_candidates, remaining_credits, estimate_basis, expires_at } ``` - 取得 preview 本身不扣點。 - 手動搜尋/巡邏、AI enrichment、批次深判與回覆生成在 UI 觸發前取得 preview;固定成本顯示確值,候選數未知時顯示範圍,不假裝精準。 - 使用者確認時送出 `preview_id` 與 `credit_ceiling`。preview 過期、產品/query plan 改版或目前餘額不足時,執行前拒絕並要求重新確認。 - 單筆既有固定成本的動作仍可由既有用量 metadata 顯示;若沒有變動成本,不必多一個確認步驟,但按鈕附近必須標示點數。 ### 5.3 實際計點、防重與失敗 1. provider 呼叫前通過會員額度、meter soft cap、preview 及 CreditCeiling gate。 2. provider 未被呼叫,或呼叫未產生既有計量規則認定的有效工作,不 commit 點數。 3. 搜尋 provider 成功返回(即使 0 hit)後,commit 搜尋點數。 4. AI 成功返回可驗證的完整判定後,commit 該候選判定點數;結果 rejected 仍計點。 5. AI 輸出結構無效或不可驗證時,沿用既有失敗退點語意,不把失敗結果加入有效判定快取。 6. 有效判定唯一鍵為 owner+來源識別+product_id+demand_input_version+map_version+judge_kind;命中即復用,重試、跨 watch 或跨 query 不再扣。 7. Job 中斷重跑時,逐筆跳過已 commit 且已有有效結果的唯一鍵;不得只靠整批成功旗標防重。 8. 達 CreditCeiling 或方案點數上限時停止新的 provider 呼叫,已完成結果保留,剩餘候選記 `budget_deferred_count`;不得把工作標成完整成功。 ### 5.4 自動巡邏成本上限 - P0 沿用方案既有雷達與 AI 配額作預設 CreditCeiling,不新增每產品自訂額度;自訂額度列 P1。 - 每日排程不逐次詢問,但開始前仍建立系統 CostPreview 快照供對帳。 - 多個 watch 共用 owner 當日剩餘額度;排程不得因先處理某個 watch 而突破會員方案上限。 - 點數不足不刪除、不封存 watch;該 Sweep 顯示 `partial_budget` 或 `blocked_budget`,並保留未判定數量。 ## 6. 使用者流程(行為) ### 6.1 第一次進入商機收件匣 1. 使用者進 `/app/radar/opportunities`,預設 `review_state=pending`、`time_scope=today`、`sort=recommended`。 2. 頁首只顯示待決定/已處理/已移除、時間範圍與排序;品牌、產品、意向、匹配狀態收進「更多篩選」。 3. 卡片首層顯示問題摘要、適合產品、單段最強證據、priority score/band、posted_at 與三個明確決策:「加入名單並追蹤/只標示已處理/不適合」。 4. 「加入名單並追蹤」成功後 OpportunityStatus=`accepted` 且 ReviewState=`completed`,顯示前往該 Contact 的入口;「只標示已處理」不建立 Contact。 5. 使用者展開完整判定後仍可直接加入名單或只標示已處理,不必關閉詳情回卡片操作。 6. 無結果時依 no watch、尚未巡邏、全部被前處理排除、點數不足、巡邏失敗、篩選後為空分別提示,不回一個無解釋的空白頁。 ### 6.2 完成、移除與復原 1. 按「只標示已處理」立即將卡片移出 pending,但不建立 Contact;操作失敗則回復原畫面並顯示後端訊息。 2. 按移除先選原因;`other` 需輸入說明。成功後顯示可復原提示,不觸發 AI。 3. 已完成/已移除 tab 使用同一列表與排序契約;復原後依 previous_review_state 回到對應 tab。 4. 加入 CRM 成功後 ReviewState 自動 completed,並回傳 Contact 入口;只標示已處理不會自動建立 Contact。兩種整理動作皆不扣點。 ### 6.3 檢查需求地圖與手動探索 1. 使用者從雷達設定或收件匣次要入口選 Brand/Product,查看免費 baseline DemandMap 與資料完整度。 2. 可免費編輯、啟停 phrases;儲存後 preview 顯示下一次探索使用的新 map version。 3. 若選 AI enrichment,先顯示 CostPreview,確認後執行;失敗不覆寫目前有效地圖。 4. 手動探索前顯示 QueryPlan 摘要、預估搜尋次數、最多 AI 候選與點數範圍;使用者確認 ceiling 後才排程。 5. 完成頁顯示 hit、dedupe、prefilter reject/review/pass、AI judge、created/merged、budget deferred 與實際點數。 ### 6.4 每日巡邏 1. worker 驗證 watch、產品與 DemandMap;incomplete 時阻擋並通知補產品資料。 2. 保存系統 preview 與版本,執行 QueryPlan 搜尋。 3. 依 §4.3 前處理,先復用已有判定,再依輕量順位及剩餘 ceiling 選 AI 深判候選。 4. 每完成一個可計點階段即記錄可續跑進度;中斷後從未完成候選繼續。 5. 建立/合併 Opportunity 時保存 priority factors、證據、版本與 ReviewState=pending;命中 removed tombstone 只合併來源。 6. 完成時寫完整 funnel 與點數;部分完成必須標 partial 並顯示未處理數。 ## 7. 介面契約 ### 7.1 共通規則與 Auth - 所有 `/api/v1/radar/**` 只接受會員 `Authorization: Bearer `;owner 隔離沿用既有安全語意。 - AI provider token/BYOK key 只供內部 provider 使用,不得出現在 CostPreview、Sweep 或 Opportunity response。 - 成功 envelope:`code=102000`、`message`、`data`、`error`;失敗不得回成功空資料。 - 列表 request `page / pageSize`,response `pagination + list`。 - 所有時間均為 UTC unix nanoseconds `int64`。 - 後端契約只由 `generate/api/*.api` 修改並以 goctl 生成 handler/route;業務行為落在 logic/module。 ### 7.2 HTTP / API | Method | Path | 用途 | 主要 request/response | |--------|------|------|-----------------------| | GET | `/api/v1/radar/opportunities` | 單一收件匣列表 | query 加 `review_state`、`time_scope`、`sort`;item 加 review、priority、evidence | | GET | `/api/v1/radar/opportunities/:id` | 商機詳情 | 回完整 review audit、priority factors、DemandEvidence 與產品依據 | | PUT | `/api/v1/radar/opportunities/:id/review-state` | 完成、移除、復原 | req `state`、`removal_reason?`、`removal_note?`、`duplicate_of_opportunity_id?`;回 Opportunity | | POST | `/api/v1/radar/opportunities/:id/accept` | 加入 CRM | Retain;成功時另設 review_state=completed | | POST | `/api/v1/radar/opportunities/:id/dismiss` | 舊客戶相容 | Retain;映射 review_state=removed,保存 reason 或 legacy_unknown | | GET | `/api/v1/radar/products/:productId/demand-map` | 讀目前需求地圖 | 回完整度、input/map version、phrases 與 stale 資訊 | | PUT | `/api/v1/radar/products/:productId/demand-map` | 免費人工調整 | req enabled/custom phrases 與基礎版本;衝突時要求重讀 | | POST | `/api/v1/radar/products/:productId/demand-map/enrich` | AI 擴充 | req `preview_id`、`credit_ceiling`;回新 map 與 credits_used | | GET | `/api/v1/radar/cost-preview` | 付費動作預估 | query `action`、watch/product/opportunity context;回 CostPreview | | POST | `/api/v1/radar/watches/:id/sweep` | 手動巡邏 | req 加 `preview_id`、`credit_ceiling`;回 job/sweep 識別 | | POST | `/api/v1/radar/explore` | 手動探索 | req 加 map version、preview、ceiling;回 funnel 統計與實際耗點 | | GET | `/api/v1/radar/sweeps` | 巡邏紀錄 | item 加版本、funnel counts、credit breakdown、budget state | | GET | `/api/v1/radar/today` | 舊頁相容 | Retain 暫時;只回 pending+today,同新列表排序,之後導覽移除 | `OpportunityPublic` 增量: ```text { review_state, previous_review_state?, removal_reason?, removal_note?, removed_at?, removed_by?, last_matched_at?, priority_score, priority_band, priority_factors: { pain_fit, demand_intent, evidence_quality, freshness }, demand_evidence[], demand_input_version?, demand_map_version? } ``` `RadarSweepPublic` 增量: ```text { demand_input_version?, demand_map_version?, hit_count, deduped_count, prefilter_pass_count, prefilter_review_count, prefilter_rejected_count, cached_judgment_count, judged_count, created_count, merged_count, tombstone_matched_count, budget_deferred_count, status: complete|partial_budget|blocked_budget|failed, credits_used, credit_breakdown: { search, demand_map, judge, reply } } ``` ### 7.3 列表 query 行為 | query | 值 | 預設/說明 | |-------|----|-------------| | `review_state` | pending/completed/removed | pending | | `time_scope` | today/7d/all | today;與 from/to 不可同時使用 | | `sort` | recommended/newest/oldest/product_fit/demand_intent | recommended | | `brand_id`、`product_id` | owner 可見識別 | 沿用既有 product match filter | | `priority_band` | high/review/low | 空表示全部 | | `from`、`to` | unix ns UTC | 以 posted_at 篩選;未知時間只在 all 且無 from/to 時出現 | - `time_scope=today` 以會員顯示時區的當地 00:00 換算成 UTC ns 範圍;資料儲存仍只用 UTC ns。 - time scope、filter、sort、page 寫入 URL,重新整理與返回頁面保持相同工作位置。 ### 7.4 前端頁面 / 導覽 | 路由 | 目的 | 主要操作 | |------|------|----------| | `/app/radar/opportunities` | 唯一商機工作收件匣 | review tab、時間、排序、產品篩選、完成、移除、復原、詳情 | | `/app/radar/today` | 舊連結相容 | redirect 到 opportunities 的 pending+today,不再顯示第二套頁面 | | `/app/radar/watches` | 雷達與需求地圖入口 | 查看完整度、編輯 map、AI enrichment、預覽成本、觸發巡邏 | | `/app/usage` | 用量追溯 | 沿用既有 meter,按 `radar.*` source 查看消耗 | - 導覽只保留一個「商機」入口,不同頁面不得重複出現同一工作清單。 - mock/live 實作同一 repository 介面、排序、狀態、錯誤與空結果。 - 沿用 Harbor Desk 與既有 UI 元件;不新增 UI framework。 ### 7.5 Job / 背景任務 | Template type | Worker | 觸發 | 成功結果 | |---------------|--------|------|----------| | `radar_sweep` | 既有 radar worker | 每日排程、首巡、手動巡 | 按新 funnel 執行,guarded 更新完整或 partial 狀態與點數 | | `radar_demand_map_enrich` | radar worker | 使用者確認付費 enrichment | 產生可驗證 phrases;成功才切換 map version 與 commit 用量 | - Redis lock value 保持 `workerID`;只有持有者可續期/釋放。 - Job status 必須 guarded 更新;被取消或已終態的 job 不得被晚到 worker 覆寫。 - CostPreview 不是 Job,不得占用工作佇列或扣點。 ### 7.6 錯誤語意 | 情境 | 行為 | |------|------| | 非 owner 讀寫商機、產品或需求地圖 | not found/forbidden 的既有安全語意,不洩漏存在性 | | review state/removal reason/sort 不合法 | validation error;不做部分更新 | | removed 未帶原因或 other 未帶 note | validation error;保持原狀態 | | duplicate 指向不存在、自己或其他 owner | validation error/not found;保持原狀態 | | DemandMap 基礎版本過期 | conflict;回目前版本摘要,要求重讀,不覆寫 | | DemandMap incomplete | 新巡邏 blocked;回缺少哪些產品資訊,不回成功空資料 | | preview 過期或 context 已改版 | conflict;要求重新預估與確認 | | ceiling 小於固定成本 | 執行前 validation/quota error;provider 不被呼叫、不扣點 | | 點數於批次中耗盡 | Sweep partial_budget;回已完成與 deferred counts | | 全部候選被前處理排除 | Sweep complete 且 created=0;回各 reject reason counts,非無說明空成功 | | provider 失敗 | 沿用 retry/退點規則;Sweep failed 或 partial,watch 不被刪除 | ## 8. 與現況對照 | 能力 | Retain | Replace | Remove | 備註 | |------|--------|---------|--------|------| | Opportunity 與 owner+來源去重 | ✓ | | | 加 tombstone 行為,不重建第二套實體 | | `intent_score`、`product_fit_score` | ✓ | | | 保留顯示;新推薦排序用獨立 priority score | | ProductMatch 與主推產品 | ✓ | | | DemandMap 提供更好的證據來源 | | `accepted` 加入 CRM | ✓ | | | 成功後同步 review completed | | `dismissed` 終態 | 相容讀寫 | ✓ | | 新 UI 改用可復原 review removed | | `RadarTodayPage` 獨立工作頁 | | | ✓ | 舊 URL redirect 到單一收件匣 | | `RadarOpportunitiesPage` | | ✓ | | 改成唯一收件匣,不再只是封存查詢頁 | | watch 手工 include/exclude terms | ✓ | | | QueryPlan 與 DemandMap 提供可追溯建議 | | 目前短詞建議 | 相容 | ✓ | | 升級為五類 DemandMap,不只回平面 terms | | 基本 substring exclude/分類 | ✓ | ✓ | | 成為完整、可觀測的免費前處理 funnel | | 五問與產品適配 AI | ✓ | ✓ | | 只處理 shortlist,輸出可驗證證據並防重扣 | | 既有四 meter、BYOK、方案配額 | ✓ | | | 只加 `radar.*` source 與預估/明細 | | 今日/全部兩套導覽 | | | ✓ | 時間改為同頁 scope | | hard delete 商機 | | | ✓ | P0 明確不提供 | ## 9. 非功能 - **安全/權限:** Brand、Product、DemandMap、Opportunity、CostPreview、Sweep 全部以 owner 隔離;不得回傳 provider token、搜尋 session 或他人存在性。 - **一致性:** review state 更新、移除 audit 與 tombstone 必須同一成功邊界;accept 的 CRM binding 與 review completed 不得出現成功訊息但狀態未更新。 - **冪等性:** review state、provider 判定與用量 commit 各自有穩定冪等鍵;HTTP 重送及 Job 重跑不重複扣點。 - **可觀測性:** 每 Sweep 保存版本、funnel counts、各階段原因、點數 breakdown、partial/blocked 原因;日誌不得含 token 或完整敏感設定。 - **效能邊界:** 列表必須 server-side filter/sort/page;前處理應在 provider 深判前完成。候選與 AI 最大數受現有搜尋 limit、方案配額與 CreditCeiling 三重上限。 - **可解釋性:** 正向高順位理由必須能回指公開原文與產品/DemandMap 依據;找不到原文片段不得保存為高順位證據。 - **相容性:** 缺新欄位的舊資料可讀;舊 sort、today、accept、dismiss 呼叫有明確映射,不要求客戶端同日切換。 ## 10. 資料保存(邏輯層) | 實體 | 關鍵欄位 | 生命週期 | |------|----------|----------| | Opportunity | review state、previous state、removal fields、priority factors、demand evidence、版本、last matched | 既有 owner+來源唯一;removed 保留 tombstone;不硬刪 | | OpportunityReviewEvent | opportunity、from/to、reason/note、duplicate target、actor、at | append-only;完成、移除、復原皆留紀錄 | | DemandMap | product、input/map version、state、五類 phrases、basis、origin | 一產品一個 current;舊版本保留供既有判定解釋與防重 | | JudgmentResult | owner、來源、product、input/map version、kind、結果、usage event | 唯一鍵防重;有效結果可復用;失敗不當有效快取 | | CostPreview | owner、action、context version、估計、ceiling、expiry | 短期保存;只可由同 owner 使用一次啟動工作,不扣點 | | RadarSweep | query/版本、funnel counts、status、credit breakdown、deferred | 保留供成本、續跑與品質分析;時間 UTC ns | | UsageEvent | 既有 meter、`radar.*` source、key mode、credits、count | Retain;不新增 meter;與 Sweep 可對帳 | ## 11. 驗收場景(Given / When / Then) 1. **Given** 既有 qualified 商機沒有 review_state,**When** 開啟收件匣,**Then** 它出現在 pending,且讀取不扣點。 2. **Given** pending 商機,**When** 使用者標完成,**Then** 它移到 completed、未建立 Contact、可免費恢復 pending。 3. **Given** pending 商機,**When** 以 pain_mismatch 移除,**Then** 它進 removed、保存原因;下次同來源命中只更新來源資訊,不新建、不重判、不扣判定點。 4. **Given** removed 商機先前為 completed,**When** 復原,**Then** 回 completed;移除 audit 仍可查。 5. **Given** 使用者按加入 CRM,**When** Contact binding 成功,**Then** OpportunityStatus=accepted 且 ReviewState=completed;只成功其中一半不得回成功。 6. **Given** 產品只有名稱、沒有痛點與情境,**When** 建 DemandMap,**Then** state=incomplete 且新巡邏被擋下,系統指出缺少資料。 7. **Given** 產品痛點有多種使用者口語,**When** QueryPlan 產生,**Then** 查詢含困擾/情境/結果語言,產品名不單獨成組。 8. **Given** 候選是同業廣告,**When** 前處理,**Then** disposition=reject、不呼叫 AI judge,Sweep reject count 增加。 9. **Given** 候選只有產品名命中,**When** 排名,**Then** pain fit 與 evidence quality 不因名稱得分,不能進 high。 10. **Given** 候選用不同說法表達同一產品痛點且引用可驗證,**When** 深判,**Then** 保存原文 evidence 與產品 basis,可依 priority 排到純關鍵字結果前。 11. **Given** 三筆相同 priority score,**When** 分頁查 recommended,**Then** 依 product fit、posted_at、id 穩定排序,跨頁無重複或跳筆。 12. **Given** posted_at 未知,**When** sort newest 或 oldest,**Then** 該筆都在已知時間之後,不冒充最新或最舊。 13. **Given** 手動探索尚未確認,**When** 取得 CostPreview,**Then** credits 不變;確認 ceiling 後才可啟動 provider 呼叫。 14. **Given** API 搜尋成功但 0 hit,**When** Sweep 完成,**Then** 搜尋點數如實計入、AI judge 為 0,畫面說明 no hit。 15. **Given** 前處理排除全部候選,**When** Sweep 完成,**Then** AI judge 點數為 0,並顯示各排除原因而非空白成功。 16. **Given** AI 完成結構有效但判定不適合,**When** 結果保存,**Then** 該次正常計點並快取 rejected 結果,重跑不再扣。 17. **Given** AI 回傳無法驗證的證據,**When** 判定失敗,**Then** 不建立有效 JudgmentResult、不 commit 該判定點,Sweep 顯示失敗。 18. **Given** Job 在第五筆後中斷,**When** 重跑,**Then** 前五筆從有效唯一鍵復用,僅未完成候選可能扣點。 19. **Given** CreditCeiling 在批次中用完,**When** 尚有候選,**Then** 停止新 provider 呼叫、保存已完成結果、status=partial_budget 並回 deferred count。 20. **Given** BYOK 會員執行相同流程,**When** 完成,**Then** 平台 credits_used=0,count、source 與 funnel 統計仍可查。 21. **Given** 產品只改名稱,**When** 同候選再命中,**Then** DemandInputVersion 不變,不允許重新扣點深判。 22. **Given** 產品痛點實質修改,**When** DemandMap 更新,**Then** input version 改變、舊 AI phrases 失效、使用者 custom phrases 待確認,重新深判前需新的 CostPreview。 23. **Given** 人工標註代表性資料,**When** 取 recommended 前十,**Then** 至少八筆同時具備需求與產品適配證據,且 provider/公告/純名稱命中不在 high。 24. **Given** 使用者從舊 `/app/radar/today` 進入,**When** 頁面載入,**Then** redirect 到 opportunities pending+today,沒有第二份狀態或列表邏輯。 ## 12. 開放規格問題 1. 無阻擋 P0 的開放問題。每產品自訂 CreditCeiling、批次操作與移除回饋調校均依 requirements 保持 P1,不在 P0 偷渡。 ## 13. 批准 - [x] Spec 已批准(2026-08-11/使用者) - 批准後下一步:建立 `plan.md`,依 Phase B mock 收件匣 → 契約 → Phase C live → Phase D 搜尋與品質閘拆里程碑;本輪不建立 tasks、不修改產品程式。