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

307 lines
23 KiB
Markdown
Raw 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.

# 相似帳號 / 對標帳號 / 找 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 時直接跳過整段,不報錯。