thread-master/backend/docs/similar-accounts-ta-plan.md

307 lines
23 KiB
Markdown
Raw Permalink Normal View History

2026-06-28 08:28:42 +00:00
# 相似帳號 / 對標帳號 / 找 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 加「來源」badgescan+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 schemaPhase 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` 新排序 casetopicHits 加權、log-scale follower bucket、`worker/job/scan_viral_test.go` web fallback casemock 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 bucket1k/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` 串接 priorityscan+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。PlacementB 流海巡獲客)刻意維持「無對標帳號」設計,本計畫不動其 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/RECENT50/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 — 相似帳號變一等公民(動 APICopyMission 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 時直接跳過整段,不報錯。