# 相似帳號 / 對標帳號 / 找 TA — 改進計畫

## ⚠ 換手進度（2026-06-28）

### ✅ Phase 1 已完成（全部測試通過）
`make fmt && make test` 全 ok；`make build-frontend` 通過。`go build ./...` 通過。

已完成項目：
- **1.1 名額放寬**：`MaxSimilarAccounts` 5 → 10（`backend/internal/library/viral/discover_accounts.go:15`）。
- **1.2 停止清空**：`analyze_copy_mission.go` 不再 `SimilarAccounts = nil`，改用 `similarAccountsFromSummary(mission.ResearchMap.SimilarAccounts)` 保留前次（`backend/internal/worker/job/analyze_copy_mission.go:128`）。
- **1.3 合併取代覆蓋**：`scan_viral.go` 改 `libviral.MergeSimilarAccounts(prev, new)`（`backend/internal/worker/job/scan_viral.go:195`）。
- **1.4 排序權重抽出**：`reference_accounts.go` 新增 `ReferenceRankWeights` + `DefaultReferenceRankWeights` + `rankScore`（log-scale follower bucket 1k/10k/100k/1M、topicHits 加權），排序用加權和；`postTopicRelevant` 改為 `topicTopicHits` 回傳 hits 並用 `normalisedTopicTerms` lower-case 取代 `topicTerms`。
- **1.5 web search fallback**：`scan_viral.go:170` `BuildReferenceAccountsFromScan` 後若 `len(referenceAccounts) < MaxSimilarAccounts` 且 `placement.WebSearchAvailable(research)` → 呼 `supplementSimilarAccountsFromWeb`（`scan_viral.go:369`），透過 `websearch.New(memberCtx.WebSearchConfig())` 建 client → `DiscoverSimilarAccounts` → `MergeSimilarAccounts` 串接。失敗安全：web search 失敗不讓 job fail。
- **1.6 ProfileURL helper**：新檔 `backend/internal/library/threadsapi/url.go` `ProfileURLFromPermalink(permalink, username)`，優先用 permalink 反推、fallback `https://www.threads.com/@<u>`（`.com` 為 canonical host）。`reference_accounts.go:137`、`enrich_accounts.go`、`discover_accounts.go` 已改用。
- **1.7 Brave StartPublishedDate**：`brave.SearchOptions` 加 `StartPublishedDate string` 欄位（forward-compat，目前 brave client 尚未傳給 API）；`websearch/client.go` braveAdapter 把 `opts.StartPublishedDate` 塞進 libbrave.SearchOptions。Exa 原本就有支援。
- **1.8 前端顯示**：`frontend/src/types/copyMission.ts` `CopySimilarAccountData` 加 `matched_source?: string[]` + `similarAccountSourceTier`/`similarAccountSourceLabel` helper；`frontend/src/lib/viralSignals.ts` 加 `similarAccountTone`；`CopyMissionDetailPage.tsx:837-878` row 加「來源」badge（scan+web/sky/success、web/sky、scan/brand）、「置信度」小字。
- **schema**：`missionentity.SimilarAccount` 加 `MatchedSource []string`（`backend/internal/model/copy_mission/domain/entity/mission.go:27`）；`domusecase.SimilarAccountSummary` 同步；`usecase.toSummary` 帶 matched；`types.CopySimilarAccountData` 同步；`logic/copy_mission/mapper.go` entity↔API 兩處對映帶 matched；`generate/api/copy_mission.api` 的 `CopySimilarAccountData` 同步（手動補，**未跑 `make gen-api`**——必要時下一手可跑 gen-api 重新產生以對齊）。
- 新檔：
  - `backend/internal/library/viral/merge_accounts.go` `MergeSimilarAccounts` + `unionMatchedSource`。
  - `backend/internal/library/threadsapi/url.go` `ProfileURLFromPermalink`。
  - `backend/internal/worker/job/similar_accounts.go` `similarAccountsFromSummary`（summary→entity 轉換 helper，給 worker 跨 rerun 保留 Previous）。

### 未做、給下一手
- **Phase 2**：相似帳號變一等公民（`Status`/`TopicRelevance`/`LastSeenAt` schema + PATCH API + worker 尊重 excluded/pinned + 前端釘選/排除按鈕 + Placement 維持不動）。詳見下方 Phase 2 章節。
- **Phase 3**：TA 樣本（留言作者採樣，`AudienceSample` + `audience.go` + worker 接入 + PATCH API + 前端「TA 樣本」區）。詳見下方 Phase 3 章節。
- **Phase 4**：跨任務記憶（後續，未規劃細節）。
- **重新 gen-api**：`generate/api/copy_mission.api` 只有 `CopySimilarAccountData` 加 `matched_source` 一處，types.go 已手動同步。若下一手要擴 API schema（Phase 2），建議跑 `make gen-api` 重新產 handler/logic/types 確保對齊。
- **新測試**：Phase 1 沒額外加新 test case。建議下一手補：`viral/merge_accounts_test.go`（`MergeSimilarAccounts` merges、preserves prev、truncates to limit、unionMatchedSource dedup）、`viral/reference_accounts_test.go` 新排序 case（topicHits 加權、log-scale follower bucket）、`worker/job/scan_viral_test.go` web fallback case（mock websearch client）。這些 test 是 Phase 1 的「補強」層，不阻塞 Phase 2 進行。

### 已動的檔案清單（Phase 1）
**Go 後端**
- `backend/internal/library/viral/discover_accounts.go`（常數、ProfileURL helper、accountCandidate.permalink）
- `backend/internal/library/viral/reference_accounts.go`（ReferenceRankWeights、topicHits、MatchedSource、ProfileURL helper、刪 topicTerms）
- `backend/internal/library/viral/enrich_accounts.go`（MatchedSource、ProfileURL helper、authorScore.permalink）
- `backend/internal/library/viral/merge_accounts.go`（**新檔**）
- `backend/internal/library/threadsapi/url.go`（**新檔**）
- `backend/internal/library/websearch/client.go`（braveAdapter 傳 StartPublishedDate）
- `backend/internal/library/brave/client.go`（SearchOptions 加 StartPublishedDate）
- `backend/internal/worker/job/scan_viral.go`（web fallback + MergeSimilarAccounts + supplementSimilarAccountsFromWeb）
- `backend/internal/worker/job/analyze_copy_mission.go`（停止清空 SimilarAccounts）
- `backend/internal/worker/job/similar_accounts.go`（**新檔**：similarAccountsFromSummary）
- `backend/internal/model/copy_mission/domain/entity/mission.go`（SimilarAccount 加 MatchedSource）
- `backend/internal/model/copy_mission/domain/usecase/usecase.go`（SimilarAccountSummary 加 MatchedSource）
- `backend/internal/model/copy_mission/usecase/usecase.go`（toSummary 帶 matched）
- `backend/internal/types/types.go`（CopySimilarAccountData 加 matched_source）
- `backend/generate/api/copy_mission.api`（CopySimilarAccountData 加 matched_source）
- `backend/internal/logic/copy_mission/mapper.go`（兩處對映帶 matched）

**前端**
- `frontend/src/types/copyMission.ts`（CopySimilarAccountData 加 matched_source + helper）
- `frontend/src/lib/viralSignals.ts`（similarAccountTone）
- `frontend/src/pages/CopyMissionDetailPage.tsx`（row 顯示 source badge、置信度小字）

### 設計選擇備忘
- `similarAccountsFromSummary` 只帶 summary 可見欄位（沒 `TopicRelevance/LastSeenAt/Status`，那些是 Phase 2）。Phase 1 schema 只補 `MatchedSource`，其他 Phase 2 欄位別在 Phase 1 先加，避免污染。
- 排序權重改成 log-scale follower bucket（1k/10k/100k/1M cut），避免 mega 帳號壓過 niche 高相關作者。`ReferenceRankWeights` 結構在 `reference_accounts.go` 內部，未注入 input，未來要調整再加。
- `ProfileURL` 從 `https://www.threads.net/@` 改 `https://www.threads.com/@`（threads.com 現在是 canonical host，前端 `frontend/src/lib/threadsLinks.ts` 用 `threadsProfileUrl(username, profileUrl)` 已能接受 `profile_url` injected，不需改）。
- `MergeSimilarAccounts` 串接 priority：新（scan+web）> 舊（保舊不被清掉），命中兩條 source 的 username `MatchedSource` 會 union 含 scan 與 web，但 **confidence 不會自動升 high**（confidence 由 `BuildReferenceAccountsFromScan` 內 strict/relaxed gate + verified + bestEngagement 算出來）。若未來要把「跨來源」當 high confidence 的訊號，需在 `MergeSimilarAccounts` 或 mapper 後處理；目前只透過前端 `MatchedSource` badge 視覺呈現。
- web search fallback 只在 `< MaxSimilarAccounts(=10)` 時觸發，避免每次 scan 都額外打 web 浪費 quota；也避免 web 來的帳號擠掉 scan 來的（因為 new 排前面）。
- Brave `StartPublishedDate` 欄位加了但**沒 map 到 Brave 的 `freshness` 參數**（Brave API freshness 接受 `d/w/m/y` 而非 ISO date，要做轉換）。Phase 1 只做資訊不丟失；實際生效需要第二階段處理 Brave adapter 的 `freshness` mapping。Exa 已有支援。

### 沒動的檔案（確認 boundary）
- Placement / Persona / Brand schema 完全沒動。
- 沒碰 `internal/handler/routes.go`、`internal/svc/service_context.go`、`internal/response`、`internal/middleware`。
- 沒動 `internal/model/copy_mission/repository`、`internal/model/copy_mission/domain/repository`。

---

> 範圍：僅 CopyMission flow。Placement（B 流海巡獲客）刻意維持「無對標帳號」設計，本計畫不動其 schema。
> 上限：`MaxSimilarAccounts` 自 5 放寬到 10。
> 遵循：`code/message/data` envelope、`page/pageSize`、`SSCCCDDD` 錯誤碼、UTC+0 nanoseconds、Redis `workerID` lock、不裸 `Update` job 狀態。
> 驗證：`go mod tidy && make fmt && go test ./...`；前端動到另執行 `make web-build`。

---

## 背景：threads-api-skill repo 對位

參考 repo `madebypan/threads-api-skill` 是**純發布工具包**（容器→publish 30s wait、reply_to_id 串貼、catbox.moe 圖床、60 天 token refresh、Chrome 自動化 setup），**沒有「找相似帳號 / 找對標 / 找 TA」功能**。它整理的 Threads Graph API endpoint 與踩坑，可用來校正下列事實：

- `me?fields=id,name,username,...` 取 self 資訊 → 目前 `threadsapi/client.go` 完全沒用，無法驗 token 對應 username。
- `keyword_search`（TOP/RECENT，50/page，無 cursor）→ 已實作；目前沒做翻頁，召回量受限。
- `media/{id}/replies` → 已實作，目前只當 outreach 標的，**沒把留言作者彙整成 TA 樣本**。
- Threads Graph API **沒有 endpoint 查任意別人的 follower / 受眾輪廓** → 「找 TA」必須繞道：把留言作者/互動者彙整成潛在受眾樣本。

---

## 目前狀態（viral 流程摸整）

- 唯一活著的相似帳號產生路徑：`scan_viral.go` job → `libviral.RunDiscover`（抓 threads 貼文）→ `BuildReferenceAccountsFromScan`（by author 聚合 + verified/follower/engagement 排序，strict gate→relaxed fallback）→ 寫入 `Mission.ResearchMap.SimilarAccounts`（`reference_accounts.go:43`）。
- `DiscoverSimilarAccounts` / `EnrichSimilarAccounts` / `ToEntitySimilarAccounts` 是 dead code（只有測試呼叫）。
- `analyze_copy_mission.go:138` 顯式 `researchMap.SimilarAccounts = nil` → 重新跑研究地圖 job 會清空相似帳號，前端沒警告。
- API 對相似帳號純唯讀：`UpdateCopyMissionReq`（`copy_mission.api:60-72`）沒 `similar_accounts` 欄位，使用者無法排除/釘選。
- 「找 TA」目前只是 LLM 產出的一段文字 `AudienceSummary`，**沒有任何「找出受眾樣本帳號」的機制**，連結構都沒有。
- 驗證缺位：`ProfileURL` 一律 `https://www.threads.net/@<username>` 拼字串（`reference_accounts.go:137`、`enrich_accounts.go:77`、`discover_accounts.go:119`），不驗真。
- 純 Threads API 模式下 `AuthorVerified=false, FollowerCount=0` 恆成立，排序權重失衡。
- 排序權重寫死：`reference_accounts.go:108-119` 固定 verified→follower→engagement，沒有主題 pillar 相似度、沒有跨任務記憶、沒有使用者可調。
- `postTopicRelevant`（`reference_accounts.go:166-180`）用裸 `strings.Contains`，中文長 label 命中精度低。

---

## Phase 1 — 資料品質低風險修正（先做）

### 1.1 名額放寬
- `backend/internal/library/viral/discover_accounts.go:14`：`MaxSimilarAccounts = 5` → `10`。
- `scan_viral.go:177`：`Limit: libviral.MaxSimilarAccounts` 維持引用常數。
- 前端已用 list render，不需改。

### 1.2 停止 `analyze_copy_mission` 清空相似帳號
- `backend/internal/worker/job/analyze_copy_mission.go:138`：刪 `researchMap.SimilarAccounts = nil`。
- 確認 `model/copy_mission/usecase/usecase.go` dot-path 更新其他 research_map 欄位時不會覆蓋 `similar_accounts`（worker 才會用 `Patch.ResearchMap` 整包，且 worker 現在會保留 SimilarAccounts）。

### 1.3 scan 結果改合併而非覆蓋
- `scan_viral.go:189-197`：`SimilarAccounts: referenceAccounts` → `SimilarAccounts: mergeSimilarAccounts(mission.ResearchMap.SimilarAccounts, referenceAccounts)`。
- 新檔 `backend/internal/library/viral/merge_accounts.go`：by username 合併，新來的更新統計、舊有保留；Phase 1 schema 還沒 `Status`，先純保舊不被清掉。
- `mergeSimilarAccounts(prev, new []missionentity.SimilarAccount) []missionentity.SimilarAccount`：
  - key = lowercase username；新>舊（更新統計欄位），舊沒被新覆蓋就保留。
  - 排序：保留新來秩序在前 N 個，舊的接在後面直到達 `MaxSimilarAccounts`。

### 1.4 排序權重抽出 + 主題相關改用命中詞數
- `reference_accounts.go:108-119`：抽出 `ReferenceRankWeights` struct（`VerifiedW/FollowerW/TotalEngagementW/BestEngagementW/TopicRelevanceW`，預設 `4/2/1/1/2`），排序 key = 加權和。
- `reference_accounts.go:166-180` `postTopicRelevant`：仍維持「命中詞數 >= 1 才納入」，但額外回傳 `topicHitCount` 給排序用。
- `referenceAuthorAgg` 加 `topicHits int`；`BuildReferenceAccountsFromScan` 內部迴圈累加。
-硼切詞暫不引入 jieba：對 CJK 用 `strings.Fields` + 拆字元組（>=2 字才當 term）即可，避免新依賴。

### 1.5 web search 補洞（復活 dead code 線）
- `scan_viral.go:172` `BuildReferenceAccountsFromScan` 之後，若 `len(referenceAccounts) < 5` 且 `memberCtx.WebSearchEnabled()`：
  ```go
  extra, err := libviral.DiscoverSimilarAccounts(ctx, websearchClient, libviral.DiscoverAccountsInput{
      SeedQuery: mission.SeedQuery,
      Brief:     mission.Brief,
      Pillars:   mission.ResearchMap.Pillars,
  })
  if err == nil && len(extra) > 0 {
      referenceAccounts = libviral.EnrichSimilarAccounts(referenceAccounts, nil, libviral.MaxSimilarAccounts)
      // 把 web 找到的也 by username 合併進去
  }
  ```
- `enrich_accounts.go`：`Source` 改成集合概念：`viral.SimilarAccount` 與 `missionentity.SimilarAccount` 都加 `MatchedSource []string`，命中 web 也命中 scan = `confidence="high"`。
- web search 失敗**不可讓 scan job 失敗**，只跳過並 log。

### 1.6 threadsapi profile URL helper
- 新檔 `backend/internal/library/threadsapi/url.go`：`ProfileURLFromPermalink(permalink, username) string`：
  - regex `threads\.(?:com|net)/@([^/]+)/post/` 從 permalink 抓 author root `https://www.threads.com/@<u>/`。
  - fallback `https://www.threads.com/@<username>`（注意 `www.threads.com` 而非 `www.threads.net` 較新且穩）。
- `reference_accounts.go:137`、`enrich_accounts.go:77`、`discover_accounts.go:119` 改用此 helper。

### 1.7 Brave adapter 補 StartPublishedDate
- `backend/internal/library/websearch/client.go:114-127` Brave adapter：把 `SearchOptions.StartPublishedDate`（即便 Brave API 不支援）也讀進 `libbrave.SearchOptions`，讓未來 Brave 端支援時自動生效。實作細節：libbrave.SearchOptions 若沒對應欄位就先無效果，但頂層 client 不能丟失資訊。

### 1.8 前端顯示新增資訊
- `frontend/src/pages/CopyMissionDetailPage.tsx:837-875` row：加 `source` badge（已有 `AccountSource` 文字時顯示 `brand`/`neutral`）、`confidence` 用 `text-muted` 小字。
- `frontend/src/lib/viralSignals.ts:42-48` `viralAccountRowClass`：依 Phase 1 schema 補 `MatchedSource` → 範含 `scan` 與 `web` 用 `--verified-hot` 樣式。
- `frontend/src/types/copyMission.ts`：`SimilarAccount` 加 `matched_source?: string[]`（Phase 1 只補這欄位，舊資料缺少不影響渲染）。
- 執行 `make web-build`。

---

## Phase 2 — 相似帳號變一等公民（動 API，CopyMission only）

### 2.1 schema 變更
`backend/internal/model/copy_mission/domain/entity/mission.go:23-35` `SimilarAccount` 加：
```go
Status         string   `bson:"status,omitempty" json:"status,omitempty"`        // recommended|pinned|excluded|promoted
TopicRelevance float64  `bson:"topic_relevance,omitempty" json:"topic_relevance,omitempty"`
LastSeenAt     int64    `bson:"last_seen_at,omitempty" json:"last_seen_at,omitempty"`
MatchedSource  []string `bson:"matched_source,omitempty" json:"matched_source,omitempty"`
```
`internal/types/types.go` 同步（重新 gen-api）。

### 2.2 API 新增
`backend/generate/api/copy_mission.api` 加：
```
@handler patchCopyMissionSimilarAccount
patch /personas/:persona_id/copy-missions/:copy_mission_id/similar-accounts/:username (PatchSimilarAccountReq) returns (CommonRes)

type PatchSimilarAccountReq {
    Status string `json:"status"`
}
```
跑 `make gen-api` 重新產 handler/logic/types。logic 走 dot-path：
```go
out[fmt.Sprintf("research_map.similar_accounts.%s.status", username)] = strings.TrimSpace(req.Status)
```
錯誤碼：CopyMission 目前沒有專屬 scope，**Phase 2 用 `Facade` (10)** 暫時頂著，待新增 `CopyMission Scope=41` 後再回頭改。**禁止裸寫數字**，一律 `errs.For(code.Facade).ResInvalidState("similar account status not allowed")`。

### 2.3 worker 尊重 status
- `reference_accounts.go:54` 開頭：`if isExcluded(in.ExcludedUsernames, post.Author) { continue }`。
- `scan_viral.go:170`：把 `mission.ResearchMap.SimilarAccounts` 中 `status=excluded` 的 username 傳進 `ReferenceAccountInput.ExcludedUsernames`。
- `merge_accounts.go`：`status=pinned` 永遠置頂，本次沒掃到也保留。

### 2.4 前端互動
- `CopyMissionDetailPage.tsx` row 加兩按鈕「釘選」「排除」，呼 `api.patch(.../similar-accounts/:username, {status})`，本地 optimistic update。
- 新檔 `frontend/src/lib/copyMissionSimilarAccounts.ts` 包 API client。
- `CopyMissionResearchOverview.tsx` 加「相似帳號」子區（從 detail 頁整合進來）。

---

## Phase 3 — TA 樣本（留言作者採樣，全新功能）

 Threads API 拿不到任意別人的受眾 → 繞道：用 `MediaReplies` 的 `ReplyCandidate.Author` 聚合成潛在受眾樣本。

### 3.1 schema 變更
`backend/internal/model/copy_mission/domain/entity/mission.go` `ResearchMap` 加：
```go
AudienceSamples []AudienceSample `bson:"audience_samples,omitempty" json:"audience_samples,omitempty"`
```
新 struct：
```go
type AudienceSample struct {
    Username       string `bson:"username" json:"username"`
    SamplePostID   string `bson:"sample_post_id,omitempty" json:"sample_post_id,omitempty"`
    SampleText     string `bson:"sample_text,omitempty" json:"sample_text,omitempty"`
    ReplyLikeCount int    `bson:"reply_like_count,omitempty" json:"reply_like_count,omitempty"`
    Appearances    int    `bson:"appearances,omitempty" json:"appearances,omitempty"`
    FirstSeenAt    int64  `bson:"first_seen_at" json:"first_seen_at"`
    LastSeenAt     int64  `bson:"last_seen_at,omitempty" json:"last_seen_at,omitempty"`
    Status         string `bson:"status,omitempty" json:"status,omitempty"`
}
```
`generate/api/copy_mission.api` 在 `CopyResearchMap` 加 `AudienceSamples []CopyAudienceSampleData`；`make gen-api`。

### 3.2 新 library
`backend/internal/library/viral/audience.go`：
```go
func BuildAudienceSamplesFromReplies(replies []placement.ReplyCandidate, opts AudienceOpts) []missionentity.AudienceSample
```
- `appearance = by username.Count`
- 過濾：自己 mission 的 author（即 SimilarAccount source）排除、純連結/數字罐頭 reply 文字過濾（`len < 5 chars` or URL-only）。
- 排序 `appearance desc → replyLikeCount desc → postedAt desc`，前 15 筆。
- confidence（前端呈現用，不入 struct）：`appearance >= 3` high、`= 2` medium、`= 1` low。

### 3.3 worker 接入
`scan_viral.go` 在 `AttachReplies` 完成後：
```go
var allReplies []placement.ReplyCandidate
for _, p := range postsWithReplies {
    allReplies = append(allReplies, p.Replies...)
}
samples := libviral.BuildAudienceSamplesFromReplies(allReplies, libviral.AudienceOpts{
    Max:             15,
    ExcludeAuthors:  similarAccountUsernames,
})
researchMap.AudienceSamples = mergeAudienceSamples(prev.Samples, samples)
```
合併邏輯與 `merge_accounts.go` 同款。

### 3.4 API
- `GET /copy-missions/:id` 已含（在 `research_map.audience_samples`）。
- `PATCH /copy-missions/:id/audience-samples/:username` body `{status}`，dot-path 更新 `research_map.audience_samples.<username>.status`。
- `make gen-api` + 邏輯同 2.2。

### 3.5 前端
- `CopyMissionResearchOverview.tsx` 新區「TA 樣本」（在「相似帳號」之後）：
  - 每筆 `@username`（連 `threadsProfileUrl`）、出現 N 篇、最近一次樣本文字。
  - 按鈕「收藏為長期受眾」（status=pinned）、「忽略」（status=excluded）。
- 新檔 `frontend/src/lib/copyMissionAudience.ts` 包 API client。
- `frontend/src/index.css` 加 `.hx-audience-samples*` token，沿用 `hx-copy-similar-accounts` 結構。

### 3.6 Phase 3 驗收
- `make web-build` 過、`go test ./...` 過。
- E2E：開 Threads API 連線 → 跑 scan job → detail page 顯示「TA 樣本」最多 15 筆、@username 連結不 404、可釘選後重跑 scan 不消失。

---

## Phase 4 — 跨任務記憶（後續，本計畫不實作）

`ThreadsAccount` 層加 `KnownAccounts map[username]AccountProfile{ FirstSeenAt, LastSeenAt, Missions[], PersonaIds[], Tags[], AvgEngagement, Status }`，scan job 結束 upsert。同一 username 在多 mission 出現 → confidence 累積、auto-suggest 為 persona benchmark；已 excluded 跨任務繼承。

---

## 移交檢查表

### 要動的檔案（Phase 1）
**Go 後端**
- `internal/library/viral/discover_accounts.go`（常數、ProfileURL helper）
- `internal/library/viral/reference_accounts.go`（排序權重、topicHits）
- `internal/library/viral/enrich_accounts.go`（MatchedSource 集合）
- `internal/library/viral/merge_accounts.go`（新檔）
- `internal/library/threadsapi/url.go`（新檔，profileURL helper）
- `internal/library/websearch/client.go`（Brave 補 StartPublishedDate）
- `internal/worker/job/scan_viral.go`（web 補洞、merge 取代覆蓋）
- `internal/worker/job/analyze_copy_mission.go:138`（停止清空）
- `internal/model/copy_mission/domain/entity/mission.go`（SimilarAccount 加 MatchedSource）
- `internal/types/types.go` 同步（gen-api 或手動加）

**前端（Phase 1.8）**
- `src/pages/CopyMissionDetailPage.tsx`（顯示 source/confidence）
- `src/lib/viralSignals.ts`（MatchedSource badge 對應）
- `src/types/copyMission.ts`（加 matched_source）

### 要動的檔案（Phase 2-3，後續 agent）
- `model/copy_mission/domain/entity/mission.go`（Status、TopicRelevance、LastSeenAt、AudienceSample、AudienceSamples）
- `generate/api/copy_mission.api`（兩支 PATCH endpoint + types）
- `internal/logic/copy_mission/patch_similar_account_logic.go` + `patch_audience_sample_logic.go`（gen 產出後手寫）
- `internal/library/viral/audience.go`（新檔）
- `internal/worker/job/scan_viral.go`（excluded 傳遞、audience samples、pinned 保留）
- `frontend/src/components/CopyMissionResearchOverview.tsx`（整合區）
- `frontend/src/lib/copyMissionSimilarAccounts.ts`、`copyMissionAudience.ts`（新）
- `frontend/src/index.css`（`.hx-audience-samples*`）

### 錯誤碼配置
- Phase 2 patch similar 錯誤：CopyMission 無專屬 scope → 先用 `errs.For(code.Facade).ResInvalidState("similar account status not allowed")`；待新增 `CopyMission=41` 後再回頭改。**禁止裸寫數字**。

### 失敗安全
- 任何 web search 呼叫失敗不能讓 scan job fail，只跳過。
- `mergeSimilarAccounts` 必須處理 prev=nil。
- `AudienceSamples`（Phase 3）在無 Threads API token 時直接跳過整段，不報錯。