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 LastSweptAt int64 `json:"last_swept_at,optional"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } ListWatchesReq { Page int `form:"page,default=1"` PageSize int `form:"pageSize,default=20"` Status string `form:"status,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"` } 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"` } SuggestWatchTermsReq { Limit int `json:"limit,optional"` } WatchTermSuggestion { Term string `json:"term"` Reason string `json:"reason"` Usage string `json:"usage"` // include | exclude } 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"` } 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"` 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 EmptyReason string `json:"empty_reason,optional"` EmptyHint string `json:"empty_hint,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"` 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"` } OverrideOpportunityReq { Id string `path:"id"` Band string `json:"band,optional"` Status string `json:"status,optional"` Note string `json:"note,optional"` } // ---------- 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 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 } 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"` FailedReason string `json:"failed_reason,optional"` CreditsUsed int `json:"credits_used"` StartedAt int64 `json:"started_at"` EndedAt int64 `json:"ended_at,optional"` } 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"` } ) @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 ListWatches get /watches (ListWatchesReq) returns (WatchListData) @handler CreateWatch post /watches (CreateWatchReq) returns (RadarWatchPublic) @handler SuggestWatchTerms post /watches/suggest (SuggestWatchTermsReq) returns (WatchSuggestData) @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 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 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 OverrideOpportunity post /opportunities/:id/override (OverrideOpportunityReq) returns (OpportunityPublic) @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) }