# T514 — watch-keyword-suggest-ai

> Status: `done`（2026-07-31）  
> Milestone: `M1`  
> Kind: `feat`  
> Est. change: `~160 lines`

## Goal

完成後系統應：`POST /api/v1/radar/watches/suggest` 依服務檔案回關鍵字建議清單，計入 `ai_copy`／`source=radar.suggest`。

## Depends on

- T510、T513

## Inputs

- Spec：`../spec.md` §4.2.2、§5.5、§9.1（RW-03）；§10 開放問題 7
- 既有：`apps/backend/internal/module/ai/**`（provider 與 BYOK）、既有痛點關鍵字工具（scout brief 相關）

## Outputs

### 程式變更（預期路徑）

| 路徑 | 動作 | 說明 |
|------|------|------|
| `apps/backend/internal/module/radar/usecase/suggest.go` | add | 組 prompt：服務項目＋地區＋案例 → 建議 `terms[]`／`exclude_terms[]` |
| `apps/backend/internal/logic/radar/suggestWatchTermsLogic.go` | edit | 呼叫 usecase、扣點、錯誤映射 |
| `apps/backend/internal/module/radar/domain/suggest.go` | add | 建議項結構（term、理由、建議用途 include／exclude） |

### 行為變更

- 未建服務檔案時回明確錯誤（無依據不亂猜）。
- 每則建議附一句理由，供使用者逐條採用；回傳不自動寫入 watch。
- 計費：`ai_copy` meter，`source=radar.suggest`；BYOK 與 platform gate 行為沿用既有。

### 決策（收斂 spec §10 問題 7）

- 產生方式：**AI 為主，並把既有痛點關鍵字工具的輸出作為 prompt 素材**（不新建第二套關鍵字引擎）。

## Out of scope

- 前端採用互動（T516）
- 依成效自動調整關鍵字（不在本輪）

## Acceptance

- [x] RW-03：有服務檔案 → 回建議清單且每項有理由
- [x] usage 紀錄 `meter=ai_copy`、`source=radar.suggest`
- [x] 指令：

```bash
cd apps/backend && go test ./internal/module/radar/... -count=1
```

## Notes（2026-07-31）

| 路徑 | 動作 |
|------|------|
| `internal/module/radar/domain/suggest.go` | add（建議項結構、`CleanSuggestions`） |
| `internal/module/radar/usecase/suggest.go` | add（prompt 組裝、AI 呼叫、解析） |
| `internal/module/radar/usecase/billing.go` | add（`charge`：預留→結算／退點） |
| `internal/logic/radar/suggest_watch_terms_logic.go` | edit |
| `internal/svc/service_context.go` | edit（AI／Usage 注入＋`radarPainTermBridge`） |
| `internal/module/radar/usecase/suggest_test.go` | add（11 則） |

- **計費：`ai_copy` meter、`source=radar.suggest`**，測試直接讀用量事件驗證兩個值，因為 `radar.` 前綴是 P1 價格校準的唯一資料來源，標錯就無法歸因。
- **AI 失敗一定退點**（`defer charge.Settle(ctx, &err)` 綁具名 error），有一則測試專門盯這件事。
- **沒有服務檔案就不呼叫 AI**，直接回明確錯誤：沒有依據的建議只是猜測，但使用者會把它當成系統的判斷。
- **沒有理由的建議項直接丟掉**，不補一句「AI 建議」湊數 —— 假理由比少一則更糟。
- **解析容忍 code fence 與開場白**（只截第一個 `[` 到最後一個 `]`）。模型偶爾會加客套話，硬要求純 JSON 會讓功能在那些回應上整個壞掉。
- **一則都解析不出來時回錯誤，不回空清單。** 空清單會被讀成「你的服務沒有關鍵字可監控」，那是錯的訊息。
- **痛點素材沿用既有海巡產品的 `pain_points`**（`radarPainTermBridge`，只讀），拿不到只記 log 不讓請求失敗；沒有第二套關鍵字引擎。
