syntax = "v1" // demand-radar: service profile / radar watches / sweeps / opportunities / replies // spec: docs/product/demand-radar/spec.md §5.2 // 未實作能力一律回 501(radarDomain.ErrNotReady),禁止 102000 空成功。 type ( // ---------- ServiceProfile ---------- ServiceItem { Name string `json:"name"` PriceMin float64 `json:"price_min,optional"` PriceMax float64 `json:"price_max,optional"` Currency string `json:"currency,optional"` } ServiceCasePublic { Title string `json:"title"` Summary string `json:"summary,optional"` Link string `json:"link,optional"` } FaqItem { Question string `json:"question"` Answer string `json:"answer"` } ServiceProfilePublic { Exists bool `json:"exists"` Services []ServiceItem `json:"services"` Cases []ServiceCasePublic `json:"cases"` Forbidden []string `json:"forbidden"` Faq []FaqItem `json:"faq"` ServiceAreas []string `json:"service_areas"` RemoteOk bool `json:"remote_ok"` Availability string `json:"availability,optional"` ToneNote string `json:"tone_note,optional"` UpdatedAt int64 `json:"updated_at,optional"` } UpsertServiceProfileReq { Services []ServiceItem `json:"services"` Cases []ServiceCasePublic `json:"cases,optional"` Forbidden []string `json:"forbidden,optional"` Faq []FaqItem `json:"faq,optional"` ServiceAreas []string `json:"service_areas,optional"` RemoteOk bool `json:"remote_ok,optional"` Availability string `json:"availability,optional"` ToneNote string `json:"tone_note,optional"` } // ---------- RadarWatch ---------- RadarWatchPublic { Id string `json:"id"` Terms []string `json:"terms"` ExcludeTerms []string `json:"exclude_terms"` Regions []string `json:"regions"` Status string `json:"status"` // active | paused | archived ContextMode string `json:"context_mode"` // generic | product BrandId string `json:"brand_id,optional"` ProductId string `json:"product_id,optional"` BrandNameSnapshot string `json:"brand_name_snapshot,optional"` ProductLabelSnapshot string `json:"product_label_snapshot,optional"` ContextBoundAt int64 `json:"context_bound_at,optional"` PauseReason string `json:"pause_reason,optional"` LastSweptAt int64 `json:"last_swept_at,optional"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` // 只在建立當下這是使用者第一組訂閱、且已排入首巡時為 true;不是持久狀態,僅供前端顯示一次性提示。 FirstSweepTriggered bool `json:"first_sweep_triggered,optional"` } ListWatchesReq { Page int `form:"page,default=1"` PageSize int `form:"pageSize,default=20"` Status string `form:"status,optional"` ContextMode string `form:"context_mode,optional"` BrandId string `form:"brand_id,optional"` ProductId string `form:"product_id,optional"` } WatchListData { List []RadarWatchPublic `json:"list"` Pagination Pagination `json:"pagination"` ActiveCount int `json:"active_count"` MaxActive int `json:"max_active"` ProfileExists bool `json:"profile_exists"` } CreateWatchReq { Terms []string `json:"terms"` ExcludeTerms []string `json:"exclude_terms,optional"` Regions []string `json:"regions,optional"` Enabled bool `json:"enabled,optional"` BrandId string `json:"brand_id,optional"` ProductId string `json:"product_id,optional"` } // Hours are 0–23 in Asia/Taipei. Empty GET means default [6] (台北 06:00). // int64:go-zero JSON 對 []int 常解不出來,會變成 400001。 RadarSchedulePublic { Hours []int64 `json:"hours"` Timezone string `json:"timezone"` } PutRadarScheduleReq { Hours []int64 `json:"hours,optional"` } UpdateWatchReq { Id string `path:"id"` Terms []string `json:"terms,optional"` ExcludeTerms []string `json:"exclude_terms,optional"` Regions []string `json:"regions,optional"` } WatchIdReq { Id string `path:"id"` } AssignProductReq { Id string `path:"id"` BrandId string `json:"brand_id"` ProductId string `json:"product_id"` } SuggestWatchTermsReq { Limit int `json:"limit,optional"` BrandId string `json:"brand_id,optional"` ProductId string `json:"product_id,optional"` } WatchTermSuggestion { Term string `json:"term"` Reason string `json:"reason"` Usage string `json:"usage"` // include | exclude BasisKind string `json:"basis_kind,optional"` BasisText string `json:"basis_text,optional"` } WatchSuggestData { List []WatchTermSuggestion `json:"list"` } TriggerSweepData { JobId string `json:"job_id"` SweepId string `json:"sweep_id,optional"` } // ---------- Opportunity ---------- OpportunityReason { Dimension string `json:"dimension"` // authenticity | intent | region | freshness | fit Score int `json:"score"` Reason string `json:"reason"` } OpportunityOverride { FromBand string `json:"from_band,optional"` ToBand string `json:"to_band,optional"` FromStatus string `json:"from_status,optional"` ToStatus string `json:"to_status,optional"` ActorUid int64 `json:"actor_uid"` At int64 `json:"at"` } ProductFitReasonPublic { Dimension string `json:"dimension"` // pain | scenario | audience | capability Score int `json:"score"` Reason string `json:"reason"` CandidateExcerpt string `json:"candidate_excerpt,optional"` ProductBasis string `json:"product_basis,optional"` } ProductMatchPublic { BrandId string `json:"brand_id"` ProductId string `json:"product_id"` BrandNameSnapshot string `json:"brand_name_snapshot"` ProductLabelSnapshot string `json:"product_label_snapshot"` BrandUpdatedAt int64 `json:"brand_updated_at"` ProductUpdatedAt int64 `json:"product_updated_at"` ProductFitScore int `json:"product_fit_score"` ProductFitBand string `json:"product_fit_band"` // strong | possible | weak Eligible bool `json:"eligible"` Excluded bool `json:"excluded"` ExcludeReason string `json:"exclude_reason,optional"` Reasons []ProductFitReasonPublic `json:"reasons"` Risks []string `json:"risks"` WatchIds []string `json:"watch_ids"` MatchedTerms []string `json:"matched_terms"` MatchedAt int64 `json:"matched_at"` } OpportunityPublic { Id string `json:"id"` WatchId string `json:"watch_id,optional"` Source string `json:"source"` // threads | manual | scout_promote SourceScoutPostId string `json:"source_scout_post_id,optional"` ExternalId string `json:"external_id"` Permalink string `json:"permalink"` AuthorHandle string `json:"author_handle"` Text string `json:"text"` PostedAt int64 `json:"posted_at"` Status string `json:"status"` // judging | qualified | rejected | accepted | dismissed IntentScore int `json:"intent_score"` IntentBand string `json:"intent_band"` // high | mid | low Reasons []OpportunityReason `json:"reasons"` RegionDetected string `json:"region_detected,optional"` RegionMatch string `json:"region_match"` // match | mismatch | unknown FreshnessHours int `json:"freshness_hours"` MatchedService string `json:"matched_service,optional"` MatchedTerms []string `json:"matched_terms"` RejectReason string `json:"reject_reason,optional"` Override *OpportunityOverride `json:"override,optional"` ContactId string `json:"contact_id,optional"` DefaultReply *ReplyVariantPublic `json:"default_reply,optional"` PrimaryBrandId string `json:"primary_brand_id,optional"` PrimaryProductId string `json:"primary_product_id,optional"` PrimaryBrandName string `json:"primary_brand_name,optional"` PrimaryProductLabel string `json:"primary_product_label,optional"` PrimaryProductFitScore int `json:"primary_product_fit_score,optional"` PrimaryProductFitBand string `json:"primary_product_fit_band,optional"` PrimaryProductOverridden bool `json:"primary_product_overridden,optional"` ProductMatches []ProductMatchPublic `json:"product_matches"` ReviewState string `json:"review_state"` // pending | completed | removed PreviousReviewState string `json:"previous_review_state,optional"` RemovalReason string `json:"removal_reason,optional"` RemovalNote string `json:"removal_note,optional"` RemovedAt int64 `json:"removed_at,optional"` RemovedBy int64 `json:"removed_by,optional"` LastMatchedAt int64 `json:"last_matched_at,optional"` PriorityScore int `json:"priority_score"` PriorityBand string `json:"priority_band"` // high | review | low PainFitScore int `json:"pain_fit_score,optional"` DemandIntentScore int `json:"demand_intent_score,optional"` EvidenceQualityScore int `json:"evidence_quality_score,optional"` FreshnessScore int `json:"freshness_score,optional"` DemandEvidence []string `json:"demand_evidence,optional"` DemandInputVersion string `json:"demand_input_version,optional"` DemandMapVersion int64 `json:"demand_map_version,optional"` CreatedAt int64 `json:"created_at"` } RadarTodayStats { Total int `json:"total"` High int `json:"high"` Mid int `json:"mid"` Low int `json:"low"` } RadarTodayData { Stats RadarTodayStats `json:"stats"` High []OpportunityPublic `json:"high"` Mid []OpportunityPublic `json:"mid"` Low []OpportunityPublic `json:"low"` TruncatedCount int `json:"truncated_count"` LastSweptAt int64 `json:"last_swept_at,optional"` // EmptyReason: not_swept_yet | all_watches_paused | no_watch | no_profile | sweep_failed | no_hit | no_eligible_product_match EmptyReason string `json:"empty_reason,optional"` EmptyHint string `json:"empty_hint,optional"` } RadarTodayReq { BrandId string `form:"brand_id,optional"` ProductId string `form:"product_id,optional"` FitBand string `form:"fit_band,optional"` } ListOpportunitiesReq { Page int `form:"page,default=1"` PageSize int `form:"pageSize,default=20"` Band string `form:"band,optional"` Status string `form:"status,optional"` WatchId string `form:"watch_id,optional"` BrandId string `form:"brand_id,optional"` ProductId string `form:"product_id,optional"` FitBand string `form:"fit_band,optional"` MatchState string `form:"match_state,optional"` ReviewState string `form:"review_state,optional"` TimeScope string `form:"time_scope,optional"` // today | 7d | all PriorityBand string `form:"priority_band,optional"` // high | review | low Sort string `form:"sort,optional"` From int64 `form:"from,optional"` To int64 `form:"to,optional"` } OpportunityListData { List []OpportunityPublic `json:"list"` Pagination Pagination `json:"pagination"` } OpportunityIdReq { Id string `path:"id"` } AcceptOpportunityReq { Id string `path:"id"` } AcceptOpportunityData { OpportunityId string `json:"opportunity_id"` ContactId string `json:"contact_id,optional"` Status string `json:"status"` } DismissOpportunityReq { Id string `path:"id"` Reason string `json:"reason,optional"` } ReviewStateReq { Id string `path:"id"` State string `json:"state"` // pending | completed | removed RemovalReason string `json:"removal_reason,optional"` RemovalNote string `json:"removal_note,optional"` DuplicateOpportunityId string `json:"duplicate_of_opportunity_id,optional"` } DemandMapProductReq { ProductId string `path:"productId"` } DemandMapPhrase { Text string `json:"text"` Kind string `json:"kind"` BasisKind string `json:"basis_kind,optional"` BasisText string `json:"basis_text,optional"` Origin string `json:"origin"` // product | user | ai Enabled bool `json:"enabled"` } DemandMapPublic { ProductId string `json:"product_id"` DemandInputVersion string `json:"demand_input_version"` MapVersion int64 `json:"map_version"` State string `json:"state"` // ready | incomplete | stale PainPhrases []DemandMapPhrase `json:"pain_phrases"` ScenarioPhrases []DemandMapPhrase `json:"scenario_phrases"` DesiredOutcomes []DemandMapPhrase `json:"desired_outcomes"` SolutionSignals []DemandMapPhrase `json:"solution_signals"` ExclusionSignals []DemandMapPhrase `json:"exclusion_signals"` SourceBasis []string `json:"source_basis"` CustomPhrases []DemandMapPhrase `json:"custom_phrases"` AiEnrichedAt int64 `json:"ai_enriched_at,optional"` UpdatedAt int64 `json:"updated_at"` } UpdateDemandMapReq { ProductId string `path:"productId"` ExpectedMapVersion int64 `json:"expected_map_version"` PainPhrases []DemandMapPhrase `json:"pain_phrases,optional"` ScenarioPhrases []DemandMapPhrase `json:"scenario_phrases,optional"` DesiredOutcomes []DemandMapPhrase `json:"desired_outcomes,optional"` SolutionSignals []DemandMapPhrase `json:"solution_signals,optional"` ExclusionSignals []DemandMapPhrase `json:"exclusion_signals,optional"` CustomPhrases []DemandMapPhrase `json:"custom_phrases,optional"` } CostPreviewReq { Action string `form:"action"` // sweep | explore | demand_map_enrich | reply WatchId string `form:"watch_id,optional"` ProductId string `form:"product_id,optional"` CandidateLimit int `form:"candidate_limit,optional"` } CostPreviewPublic { PreviewId string `json:"preview_id"` Action string `json:"action"` KeyMode string `json:"key_mode"` // platform | byok FixedCredits int `json:"fixed_credits"` MinCredits int `json:"min_credits"` MaxCredits int `json:"max_credits"` SearchCalls int `json:"search_calls"` MaxAiCandidates int `json:"max_ai_candidates"` RemainingCredits int `json:"remaining_credits"` EstimateBasis string `json:"estimate_basis"` ExpiresAt int64 `json:"expires_at"` } EnrichDemandMapReq { ProductId string `path:"productId"` PreviewId string `json:"preview_id"` CreditCeiling int `json:"credit_ceiling"` ExpectedMapVersion int64 `json:"expected_map_version"` } OverrideOpportunityReq { Id string `path:"id"` Band string `json:"band,optional"` Status string `json:"status,optional"` Note string `json:"note,optional"` } SetPrimaryProductReq { Id string `path:"id"` ProductId string `json:"product_id"` Reason string `json:"reason"` } // ---------- ReplyVariant ---------- ReplyVariantPublic { Id string `json:"id"` OpportunityId string `json:"opportunity_id"` Variant string `json:"variant"` // public_comment | dm | no_sales | professional | humorous Text string `json:"text"` UsedAt int64 `json:"used_at,optional"` SentChannel string `json:"sent_channel,optional"` // outbox | manual_copy // 只在 sent_channel=outbox 且真的排入既有 Outbox 佇列時才有值。 OutboxId string `json:"outbox_id,optional"` CreatedAt int64 `json:"created_at"` } ListRepliesReq { Id string `path:"id"` } ReplyListData { List []ReplyVariantPublic `json:"list"` } CreateReplyReq { Id string `path:"id"` Variant string `json:"variant"` } // MarkReplyUsedReq: channel=outbox|manual_copy(dm 僅 manual_copy) MarkReplyUsedReq { Id string `path:"id"` ReplyId string `path:"replyId"` Channel string `json:"channel"` // outbox | manual_copy // channel=outbox 時必填:要用哪個 Threads 帳號送出。 AccountId string `json:"account_id,optional"` } MarkReplyUsedData { Reply ReplyVariantPublic `json:"reply"` HealthAdvice string `json:"health_advice,optional"` } // ---------- RadarSweep ---------- RadarSweepPublic { Id string `json:"id"` WatchId string `json:"watch_id"` JobId string `json:"job_id,optional"` Path string `json:"path"` // api | crawler HitCount int `json:"hit_count"` JudgedCount int `json:"judged_count"` CreatedCount int `json:"created_count"` TruncatedCount int `json:"truncated_count"` MatchEvaluatedCount int `json:"match_evaluated_count"` MatchMergedCount int `json:"match_merged_count"` FitRejectedCount int `json:"fit_rejected_count"` FailedReason string `json:"failed_reason,optional"` CreditsUsed int `json:"credits_used"` StartedAt int64 `json:"started_at"` EndedAt int64 `json:"ended_at,optional"` DemandInputVersion string `json:"demand_input_version,optional"` DemandMapVersion int64 `json:"demand_map_version,optional"` DedupedCount int `json:"deduped_count"` PrefilterPassCount int `json:"prefilter_pass_count"` PrefilterReviewCount int `json:"prefilter_review_count"` PrefilterRejectedCount int `json:"prefilter_rejected_count"` CachedJudgmentCount int `json:"cached_judgment_count"` TombstoneMatchedCount int `json:"tombstone_matched_count"` BudgetDeferredCount int `json:"budget_deferred_count"` SweepStatus string `json:"status"` // complete | partial_budget | blocked_budget | failed CreditSearch int `json:"credit_search"` CreditDemandMap int `json:"credit_demand_map"` CreditJudge int `json:"credit_judge"` CreditReply int `json:"credit_reply"` } ListSweepsReq { Page int `form:"page,default=1"` PageSize int `form:"pageSize,default=20"` WatchId string `form:"watch_id,optional"` From int64 `form:"from,optional"` To int64 `form:"to,optional"` } SweepListData { List []RadarSweepPublic `json:"list"` Pagination Pagination `json:"pagination"` } // ---------- Manual Import(P1:不承諾全平台自動抓取的合規補位) ---------- ImportOpportunityItem { Url string `json:"url"` // 貼文內文;沒有官方 API/爬蟲可讀任意網址,判定一律吃使用者貼上的文字。 Text string `json:"text"` // 缺省時從 threads 網址猜 @handle,猜不到就留空。 Author string `json:"author,optional"` // unix nanoseconds UTC;缺省用匯入當下時間(新鮮度以匯入時間計)。 PostedAt int64 `json:"posted_at,optional"` } ImportOpportunitiesReq { Items []ImportOpportunityItem `json:"items"` BrandId string `json:"brand_id,optional"` ProductId string `json:"product_id,optional"` } ImportedOpportunityResult { Url string `json:"url"` OpportunityId string `json:"opportunity_id,optional"` Status string `json:"status"` // qualified | rejected | skipped | failed IntentBand string `json:"intent_band,optional"` IntentScore int `json:"intent_score,optional"` // skipped/failed 時的人話原因;qualified/rejected 時通常留空。 Error string `json:"error,optional"` } ImportOpportunitiesData { Results []ImportedOpportunityResult `json:"results"` } // ---------- Explore(商機頁立即探索:短詞 fan-out → 五問判定) ---------- ExploreOpportunitiesReq { // 1–6 組 Threads 短詞(契約 A);不合規整組擋下,不默默修正。 Terms []string `json:"terms"` BrandId string `json:"brand_id,optional"` ProductId string `json:"product_id,optional"` PreviewId string `json:"preview_id,optional"` CreditCeiling int `json:"credit_ceiling,optional"` DemandInputVersion string `json:"demand_input_version,optional"` DemandMapVersion int64 `json:"demand_map_version,optional"` } ExploreOpportunitiesData { SweepId string `json:"sweep_id"` HitCount int `json:"hit_count"` JudgedCount int `json:"judged_count"` CreatedCount int `json:"created_count"` MatchedCount int `json:"matched_count"` MergedCount int `json:"merged_count"` TruncatedCount int `json:"truncated_count"` CreditsUsed int `json:"credits_used"` DedupedCount int `json:"deduped_count"` PrefilterPassCount int `json:"prefilter_pass_count"` PrefilterReviewCount int `json:"prefilter_review_count"` PrefilterRejectedCount int `json:"prefilter_rejected_count"` CachedJudgmentCount int `json:"cached_judgment_count"` TombstoneMatchedCount int `json:"tombstone_matched_count"` BudgetDeferredCount int `json:"budget_deferred_count"` SweepStatus string `json:"status"` } ) @server ( group: radar prefix: /api/v1/radar middleware: AuthJWT ) service gateway { @handler GetServiceProfile get /service-profile returns (ServiceProfilePublic) @handler UpsertServiceProfile put /service-profile (UpsertServiceProfileReq) returns (ServiceProfilePublic) @handler GetRadarSchedule get /schedule returns (RadarSchedulePublic) @handler PutRadarSchedule put /schedule (PutRadarScheduleReq) returns (RadarSchedulePublic) @handler ListWatches get /watches (ListWatchesReq) returns (WatchListData) @handler CreateWatch post /watches (CreateWatchReq) returns (RadarWatchPublic) @handler SuggestWatchTerms post /watches/suggest (SuggestWatchTermsReq) returns (WatchSuggestData) @handler AssignWatchProduct post /watches/:id/assign-product (AssignProductReq) returns (RadarWatchPublic) @handler GetWatch get /watches/:id (WatchIdReq) returns (RadarWatchPublic) @handler UpdateWatch put /watches/:id (UpdateWatchReq) returns (RadarWatchPublic) @handler ArchiveWatch delete /watches/:id (WatchIdReq) returns (OkData) @handler DeleteArchivedWatch delete /watches/:id/purge (WatchIdReq) returns (OkData) @handler PauseWatch post /watches/:id/pause (WatchIdReq) returns (RadarWatchPublic) @handler ResumeWatch post /watches/:id/resume (WatchIdReq) returns (RadarWatchPublic) @handler TriggerWatchSweep post /watches/:id/sweep (WatchIdReq) returns (TriggerSweepData) @handler GetRadarToday get /today (RadarTodayReq) returns (RadarTodayData) @handler ListOpportunities get /opportunities (ListOpportunitiesReq) returns (OpportunityListData) @handler GetOpportunity get /opportunities/:id (OpportunityIdReq) returns (OpportunityPublic) @handler AcceptOpportunity post /opportunities/:id/accept (AcceptOpportunityReq) returns (AcceptOpportunityData) @handler DismissOpportunity post /opportunities/:id/dismiss (DismissOpportunityReq) returns (OpportunityPublic) @handler UpdateOpportunityReviewState put /opportunities/:id/review-state (ReviewStateReq) returns (OpportunityPublic) @handler OverrideOpportunity post /opportunities/:id/override (OverrideOpportunityReq) returns (OpportunityPublic) @handler SetPrimaryProduct put /opportunities/:id/primary-product (SetPrimaryProductReq) returns (OpportunityPublic) @handler GetDemandMap get /products/:productId/demand-map (DemandMapProductReq) returns (DemandMapPublic) @handler UpdateDemandMap put /products/:productId/demand-map (UpdateDemandMapReq) returns (DemandMapPublic) @handler EnrichDemandMap post /products/:productId/demand-map/enrich (EnrichDemandMapReq) returns (DemandMapPublic) @handler GetRadarCostPreview get /cost-preview (CostPreviewReq) returns (CostPreviewPublic) @handler ListOpportunityReplies get /opportunities/:id/replies (ListRepliesReq) returns (ReplyListData) @handler CreateOpportunityReply post /opportunities/:id/replies (CreateReplyReq) returns (ReplyVariantPublic) @handler MarkOpportunityReplyUsed post /opportunities/:id/replies/:replyId/mark-used (MarkReplyUsedReq) returns (MarkReplyUsedData) @handler ListSweeps get /sweeps (ListSweepsReq) returns (SweepListData) @handler ImportOpportunities post /import (ImportOpportunitiesReq) returns (ImportOpportunitiesData) @handler ExploreOpportunities post /explore (ExploreOpportunitiesReq) returns (ExploreOpportunitiesData) }