# 相似帳號 / 對標帳號 / 找 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/@`(`.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/@` 拼字串(`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/@/`。 - fallback `https://www.threads.com/@`(注意 `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..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 時直接跳過整段,不報錯。