75 lines
2.5 KiB
Go
75 lines
2.5 KiB
Go
package usecase
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"apps/backend/internal/module/radar/domain"
|
||
usageDomain "apps/backend/internal/module/usage/domain"
|
||
)
|
||
|
||
// GetCostPreview is deliberately read-only: it resolves the likely key mode
|
||
// and reads the current balance, but never calls PrepareCall or reserves a
|
||
// credit. The actual operation remains the only place that meters usage.
|
||
func (s *Service) GetCostPreview(ctx context.Context, ownerUID int64, action, watchID, productID string, candidateLimit int) (*domain.CostPreview, error) {
|
||
if ownerUID <= 0 {
|
||
return nil, fmt.Errorf("%w: owner_uid required", domain.ErrValidation)
|
||
}
|
||
action = strings.ToLower(strings.TrimSpace(action))
|
||
if action != "sweep" && action != "explore" && action != "demand_map_enrich" && action != "reply" {
|
||
return nil, fmt.Errorf("%w: unknown cost preview action %q", domain.ErrValidation, action)
|
||
}
|
||
if candidateLimit <= 0 {
|
||
candidateLimit = 20
|
||
}
|
||
if candidateLimit > 100 {
|
||
candidateLimit = 100
|
||
}
|
||
searchCalls := 0
|
||
if action == "sweep" || action == "explore" {
|
||
searchCalls = 1
|
||
if productID != "" {
|
||
if plan, err := s.BuildProductQueryPlan(ctx, ownerUID, productID); err == nil && len(plan.Groups) > 0 {
|
||
searchCalls = len(plan.Groups)
|
||
}
|
||
}
|
||
}
|
||
maxAI := 0
|
||
fixed := 0
|
||
min := searchCalls * usageDomain.MeterCost(usageDomain.MeterWebSearch)
|
||
max := min
|
||
switch action {
|
||
case "sweep", "explore":
|
||
maxAI = candidateLimit
|
||
max += maxAI * usageDomain.MeterCost(usageDomain.MeterAIResearch)
|
||
case "demand_map_enrich", "reply":
|
||
fixed = usageDomain.MeterCost(usageDomain.MeterAICopy)
|
||
min, max = fixed, fixed
|
||
}
|
||
mode := usageDomain.KeyModePlatform
|
||
meter := usageDomain.MeterWebSearch
|
||
if action == "demand_map_enrich" || action == "reply" {
|
||
meter = usageDomain.MeterAICopy
|
||
}
|
||
if s.ResolveKey != nil {
|
||
if resolved, _, err := s.ResolveKey(ctx, ownerUID, meter); err == nil && resolved != "" {
|
||
mode = resolved
|
||
}
|
||
}
|
||
remaining := 0
|
||
if s.Usage != nil {
|
||
if summary, err := s.Usage.GetSummary(ctx, ownerUID, ""); err == nil && summary != nil {
|
||
remaining = summary.Platform.CreditsRemaining
|
||
}
|
||
}
|
||
return &domain.CostPreview{
|
||
PreviewID: domain.NewID(), Action: action, KeyMode: mode,
|
||
FixedCredits: fixed, MinCredits: min, MaxCredits: max,
|
||
SearchCalls: searchCalls, MaxAICandidates: maxAI, RemainingCredits: remaining,
|
||
EstimateBasis: "免費前處理;provider 成功後依 web_search/ai_research/ai_copy meter 計點",
|
||
ExpiresAt: time.Now().UTC().Add(5 * time.Minute).UnixNano(),
|
||
}, nil
|
||
}
|