Files
2026-05-12 18:49:52 +08:00

194 lines
5.6 KiB
Go

package discovery
import (
"context"
"log"
"time"
"supply-intelligence/internal/domain"
"supply-intelligence/internal/integration"
)
// SchedulerTrigger defines how discovery is invoked
type SchedulerTrigger int
const (
TriggerManual SchedulerTrigger = iota
TriggerScheduled
TriggerNewAccount
)
// SupplierAdapterRegistry holds all registered platform adapters
type SupplierAdapterRegistry struct {
adapters map[string]integration.SupplierAdapter
}
func NewSupplierAdapterRegistry() *SupplierAdapterRegistry {
return &SupplierAdapterRegistry{adapters: make(map[string]integration.SupplierAdapter)}
}
func (r *SupplierAdapterRegistry) Register(adapter integration.SupplierAdapter) {
r.adapters[adapter.Platform()] = adapter
}
func (r *SupplierAdapterRegistry) Get(platform string) (integration.SupplierAdapter, bool) {
adapter, ok := r.adapters[platform]
return adapter, ok
}
func (r *SupplierAdapterRegistry) ListPlatforms() []string {
platforms := make([]string, 0, len(r.adapters))
for p := range r.adapters {
platforms = append(platforms, p)
}
return platforms
}
// ScanResult holds the outcome of a platform scan
type ScanResult struct {
Platform string
NewModels int
RemovedModels []string // models that were in candidates but not in supplier list
Errors []string
}
// DiscoveryScheduler orchestrates periodic and on-demand discovery scans
type DiscoveryScheduler struct {
service *Service
registry *SupplierAdapterRegistry
repo AccountLister
now func() time.Time
}
// AccountLister is implemented by repository.Repository
type AccountLister interface {
ListActiveAccounts(ctx context.Context) []domain.AccountRoutingState
ListSupplyAccountsByPlatform(ctx context.Context, platform string) []domain.SupplyAccount
}
func NewDiscoveryScheduler(service *Service, registry *SupplierAdapterRegistry, repo AccountLister) *DiscoveryScheduler {
return &DiscoveryScheduler{
service: service,
registry: registry,
repo: repo,
now: func() time.Time { return time.Now().UTC() },
}
}
// ScanAllPlatforms runs discovery across all registered platforms
func (s *DiscoveryScheduler) ScanAllPlatforms(ctx context.Context) ([]ScanResult, error) {
platforms := s.registry.ListPlatforms()
results := make([]ScanResult, 0, len(platforms))
for _, platform := range platforms {
result, err := s.ScanPlatform(ctx, platform)
if err != nil {
results = append(results, ScanResult{Platform: platform, Errors: []string{err.Error()}})
continue
}
results = append(results, *result)
}
return results, nil
}
// ScanPlatform runs discovery for a single platform
func (s *DiscoveryScheduler) ScanPlatform(ctx context.Context, platform string) (*ScanResult, error) {
adapter, ok := s.registry.Get(platform)
if !ok {
return nil, ErrPlatformNotSupported
}
result := &ScanResult{Platform: platform}
// Get models from the platform
// In production these accounts come from the database; here we accept a map for injection
accounts := s.loadAccountsForPlatform(ctx, platform)
if len(accounts) == 0 {
log.Printf("[discovery] no accounts registered for platform %s, skipping", platform)
return result, nil
}
// Use the first account as the source of models (in production would fan out)
account := accounts[0]
models, err := adapter.GetModels(ctx, account)
if err != nil {
result.Errors = append(result.Errors, "GetModels: "+err.Error())
return result, err
}
log.Printf("[discovery] platform=%s found %d models", platform, len(models))
// Record each model as a candidate
for _, model := range models {
candidateInput := RecordCandidateInput{
CandidateID: platform + "-" + model.ModelID,
AccountID: account.AccountID,
Platform: platform,
Model: model.ModelID,
Source: "official_api",
DiscoveredAt: s.now(),
}
out, err := s.service.RecordCandidate(ctx, candidateInput)
if err != nil {
result.Errors = append(result.Errors, "RecordCandidate: "+err.Error())
continue
}
if out.Created {
result.NewModels++
log.Printf("[discovery] new candidate: platform=%s model=%s", platform, model.ModelID)
}
}
return result, nil
}
// loadAccountsForPlatform returns supplier accounts for a platform
func (s *DiscoveryScheduler) loadAccountsForPlatform(ctx context.Context, platform string) []integration.SupplierAccount {
if s.repo == nil {
// Fallback: return a default account when repo is not configured
return []integration.SupplierAccount{
{AccountID: 1, Platform: platform, APIKey: "", BaseURL: defaultBaseURL(platform)},
}
}
// Prefer supply_accounts (has API key)
supplyAccounts := s.repo.ListSupplyAccountsByPlatform(ctx, platform)
if len(supplyAccounts) > 0 {
accounts := make([]integration.SupplierAccount, 0, len(supplyAccounts))
for _, acc := range supplyAccounts {
accounts = append(accounts, integration.SupplierAccount{
AccountID: acc.AccountID,
Platform: acc.Platform,
APIKey: acc.APIKey,
BaseURL: defaultBaseURL(platform),
})
}
return accounts
}
// Fallback: routing states (API key may be empty)
allAccounts := s.repo.ListActiveAccounts(ctx)
var accounts []integration.SupplierAccount
for _, acc := range allAccounts {
if acc.Platform == platform {
accounts = append(accounts, integration.SupplierAccount{
AccountID: acc.AccountID,
Platform: acc.Platform,
APIKey: acc.APIKey,
BaseURL: defaultBaseURL(platform),
})
}
}
return accounts
}
func defaultBaseURL(platform string) string {
switch platform {
case "openai":
return "https://api.openai.com"
case "anthropic":
return "https://api.anthropic.com"
default:
return ""
}
}