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

23 KiB
Raw Blame History

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

⚠ 換手進度2026-06-28

Phase 1 已完成(全部測試通過)

make fmt && make test 全 okmake build-frontend 通過。go build ./... 通過。

已完成項目:

  • 1.1 名額放寬MaxSimilarAccounts 5 → 10backend/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.golibviral.MergeSimilarAccounts(prev, new)backend/internal/worker/job/scan_viral.go:195)。
  • 1.4 排序權重抽出reference_accounts.go 新增 ReferenceRankWeights + DefaultReferenceRankWeights + rankScorelog-scale follower bucket 1k/10k/100k/1M、topicHits 加權),排序用加權和;postTopicRelevant 改為 topicTopicHits 回傳 hits 並用 normalisedTopicTerms lower-case 取代 topicTerms
  • 1.5 web search fallbackscan_viral.go:170 BuildReferenceAccountsFromScan 後若 len(referenceAccounts) < MaxSimilarAccountsplacement.WebSearchAvailable(research) → 呼 supplementSimilarAccountsFromWebscan_viral.go:369),透過 websearch.New(memberCtx.WebSearchConfig()) 建 client → DiscoverSimilarAccountsMergeSimilarAccounts 串接。失敗安全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 hostreference_accounts.go:137enrich_accounts.godiscover_accounts.go 已改用。
  • 1.7 Brave StartPublishedDatebrave.SearchOptionsStartPublishedDate string 欄位forward-compat目前 brave client 尚未傳給 APIwebsearch/client.go braveAdapter 把 opts.StartPublishedDate 塞進 libbrave.SearchOptions。Exa 原本就有支援。
  • 1.8 前端顯示frontend/src/types/copyMission.ts CopySimilarAccountDatamatched_source?: string[] + similarAccountSourceTier/similarAccountSourceLabel helperfrontend/src/lib/viralSignals.tssimilarAccountToneCopyMissionDetailPage.tsx:837-878 row 加「來源」badgescan+web/sky/success、web/sky、scan/brand、「置信度」小字。
  • schemamissionentity.SimilarAccountMatchedSource []stringbackend/internal/model/copy_mission/domain/entity/mission.go:27domusecase.SimilarAccountSummary 同步;usecase.toSummary 帶 matchedtypes.CopySimilarAccountData 同步;logic/copy_mission/mapper.go entity↔API 兩處對映帶 matchedgenerate/api/copy_mission.apiCopySimilarAccountData 同步(手動補,未跑 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 similarAccountsFromSummarysummary→entity 轉換 helper給 worker 跨 rerun 保留 Previous

未做、給下一手

  • Phase 2:相似帳號變一等公民(Status/TopicRelevance/LastSeenAt schema + PATCH API + worker 尊重 excluded/pinned + 前端釘選/排除按鈕 + Placement 維持不動)。詳見下方 Phase 2 章節。
  • Phase 3TA 樣本(留言作者採樣,AudienceSample + audience.go + worker 接入 + PATCH API + 前端「TA 樣本」區)。詳見下方 Phase 3 章節。
  • Phase 4:跨任務記憶(後續,未規劃細節)。
  • 重新 gen-apigenerate/api/copy_mission.api 只有 CopySimilarAccountDatamatched_source 一處types.go 已手動同步。若下一手要擴 API schemaPhase 2建議跑 make gen-api 重新產 handler/logic/types 確保對齊。
  • 新測試Phase 1 沒額外加新 test case。建議下一手補viral/merge_accounts_test.goMergeSimilarAccounts merges、preserves prev、truncates to limit、unionMatchedSource dedupviral/reference_accounts_test.go 新排序 casetopicHits 加權、log-scale follower bucketworker/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.goReferenceRankWeights、topicHits、MatchedSource、ProfileURL helper、刪 topicTerms
  • backend/internal/library/viral/enrich_accounts.goMatchedSource、ProfileURL helper、authorScore.permalink
  • backend/internal/library/viral/merge_accounts.go新檔
  • backend/internal/library/threadsapi/url.go新檔
  • backend/internal/library/websearch/client.gobraveAdapter 傳 StartPublishedDate
  • backend/internal/library/brave/client.goSearchOptions 加 StartPublishedDate
  • backend/internal/worker/job/scan_viral.goweb 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.goSimilarAccount 加 MatchedSource
  • backend/internal/model/copy_mission/domain/usecase/usecase.goSimilarAccountSummary 加 MatchedSource
  • backend/internal/model/copy_mission/usecase/usecase.gotoSummary 帶 matched
  • backend/internal/types/types.goCopySimilarAccountData 加 matched_source
  • backend/generate/api/copy_mission.apiCopySimilarAccountData 加 matched_source
  • backend/internal/logic/copy_mission/mapper.go(兩處對映帶 matched

前端

  • frontend/src/types/copyMission.tsCopySimilarAccountData 加 matched_source + helper
  • frontend/src/lib/viralSignals.tssimilarAccountTone
  • frontend/src/pages/CopyMissionDetailPage.tsxrow 顯示 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未來要調整再加。
  • ProfileURLhttps://www.threads.net/@https://www.threads.com/@threads.com 現在是 canonical host前端 frontend/src/lib/threadsLinks.tsthreadsProfileUrl(username, profileUrl) 已能接受 profile_url injected不需改
  • MergeSimilarAccounts 串接 priorityscan+web> 舊(保舊不被清掉),命中兩條 source 的 username MatchedSource 會 union 含 scan 與 webconfidence 不會自動升 highconfidence 由 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.gointernal/svc/service_context.gointernal/responseinternal/middleware
  • 沒動 internal/model/copy_mission/repositoryinternal/model/copy_mission/domain/repository

範圍:僅 CopyMission flow。PlacementB 流海巡獲客)刻意維持「無對標帳號」設計,本計畫不動其 schema。 上限:MaxSimilarAccounts 自 5 放寬到 10。 遵循:code/message/data envelope、page/pageSizeSSCCCDDD 錯誤碼、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_searchTOP/RECENT50/page無 cursor→ 已實作;目前沒做翻頁,召回量受限。
  • media/{id}/replies → 已實作,目前只當 outreach 標的,沒把留言作者彙整成 TA 樣本
  • Threads Graph API 沒有 endpoint 查任意別人的 follower / 受眾輪廓 → 「找 TA」必須繞道把留言作者/互動者彙整成潛在受眾樣本。

目前狀態viral 流程摸整)

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

Phase 1 — 資料品質低風險修正(先做)

1.1 名額放寬

  • backend/internal/library/viral/discover_accounts.go:14MaxSimilarAccounts = 510
  • scan_viral.go:177Limit: 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_accountsworker 才會用 Patch.ResearchMap 整包,且 worker 現在會保留 SimilarAccounts

1.3 scan 結果改合併而非覆蓋

  • scan_viral.go:189-197SimilarAccounts: referenceAccountsSimilarAccounts: mergeSimilarAccounts(mission.ResearchMap.SimilarAccounts, referenceAccounts)
  • 新檔 backend/internal/library/viral/merge_accounts.goby 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 structVerifiedW/FollowerW/TotalEngagementW/BestEngagementW/TopicRelevanceW,預設 4/2/1/1/2),排序 key = 加權和。
  • reference_accounts.go:166-180 postTopicRelevant:仍維持「命中詞數 >= 1 才納入」,但額外回傳 topicHitCount 給排序用。
  • referenceAuthorAggtopicHits intBuildReferenceAccountsFromScan 內部迴圈累加。 -硼切詞暫不引入 jieba對 CJK 用 strings.Fields + 拆字元組(>=2 字才當 term即可避免新依賴。

1.5 web search 補洞(復活 dead code 線)

  • scan_viral.go:172 BuildReferenceAccountsFromScan 之後,若 len(referenceAccounts) < 5memberCtx.WebSearchEnabled()
    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.goSource 改成集合概念:viral.SimilarAccountmissionentity.SimilarAccount 都加 MatchedSource []string,命中 web 也命中 scan = confidence="high"
  • web search 失敗不可讓 scan job 失敗,只跳過並 log。

1.6 threadsapi profile URL helper

  • 新檔 backend/internal/library/threadsapi/url.goProfileURLFromPermalink(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:137enrich_accounts.go:77discover_accounts.go:119 改用此 helper。

1.7 Brave adapter 補 StartPublishedDate

  • backend/internal/library/websearch/client.go:114-127 Brave adapterSearchOptions.StartPublishedDate(即便 Brave API 不支援)也讀進 libbrave.SearchOptions,讓未來 Brave 端支援時自動生效。實作細節libbrave.SearchOptions 若沒對應欄位就先無效果,但頂層 client 不能丟失資訊。

1.8 前端顯示新增資訊

  • frontend/src/pages/CopyMissionDetailPage.tsx:837-875 rowsource badge已有 AccountSource 文字時顯示 brand/neutral)、confidencetext-muted 小字。
  • frontend/src/lib/viralSignals.ts:42-48 viralAccountRowClass:依 Phase 1 schema 補 MatchedSource → 範含 scanweb--verified-hot 樣式。
  • frontend/src/types/copyMission.tsSimilarAccountmatched_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 加:

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

out[fmt.Sprintf("research_map.similar_accounts.%s.status", username)] = strings.TrimSpace(req.Status)

錯誤碼CopyMission 目前沒有專屬 scopePhase 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.SimilarAccountsstatus=excluded 的 username 傳進 ReferenceAccountInput.ExcludedUsernames
  • merge_accounts.gostatus=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 拿不到任意別人的受眾 → 繞道:用 MediaRepliesReplyCandidate.Author 聚合成潛在受眾樣本。

3.1 schema 變更

backend/internal/model/copy_mission/domain/entity/mission.go ResearchMap 加:

AudienceSamples []AudienceSample `bson:"audience_samples,omitempty" json:"audience_samples,omitempty"`

新 struct

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.apiCopyResearchMapAudienceSamples []CopyAudienceSampleDatamake gen-api

3.2 新 library

backend/internal/library/viral/audience.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前端呈現用不入 structappearance >= 3 high、= 2 medium、= 1 low。

3.3 worker 接入

scan_viral.goAttachReplies 完成後:

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.goMatchedSource 集合)
  • internal/library/viral/merge_accounts.go(新檔)
  • internal/library/threadsapi/url.go新檔profileURL helper
  • internal/library/websearch/client.goBrave 補 StartPublishedDate
  • internal/worker/job/scan_viral.goweb 補洞、merge 取代覆蓋)
  • internal/worker/job/analyze_copy_mission.go:138(停止清空)
  • internal/model/copy_mission/domain/entity/mission.goSimilarAccount 加 MatchedSource
  • internal/types/types.go 同步gen-api 或手動加)

前端Phase 1.8

  • src/pages/CopyMissionDetailPage.tsx(顯示 source/confidence
  • src/lib/viralSignals.tsMatchedSource badge 對應)
  • src/types/copyMission.ts(加 matched_source

要動的檔案Phase 2-3後續 agent

  • model/copy_mission/domain/entity/mission.goStatus、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.gogen 產出後手寫)
  • internal/library/viral/audience.go(新檔)
  • internal/worker/job/scan_viral.goexcluded 傳遞、audience samples、pinned 保留)
  • frontend/src/components/CopyMissionResearchOverview.tsx(整合區)
  • frontend/src/lib/copyMissionSimilarAccounts.tscopyMissionAudience.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。
  • AudienceSamplesPhase 3在無 Threads API token 時直接跳過整段,不報錯。