Files
supply-intelligence/internal/discovery/scheduler.go
2026-05-07 10:16:46 +08:00

162 lines
4.5 KiB
Go

package discovery
import (
"context"
"log"
"time"
"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
now func() time.Time
}
func NewDiscoveryScheduler(service *Service, registry *SupplierAdapterRegistry) *DiscoveryScheduler {
return &DiscoveryScheduler{
service: service,
registry: registry,
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
// In production this queries the accounts table; here it returns a seeded default
func (s *DiscoveryScheduler) loadAccountsForPlatform(ctx context.Context, platform string) []integration.SupplierAccount {
// Production: query supply_accounts where platform = X and status = active
// For now: return a placeholder that will work with adapter.GetModels
return []integration.SupplierAccount{
{
AccountID: 1,
Platform: platform,
APIKey: "",
BaseURL: defaultBaseURL(platform),
},
}
}
func defaultBaseURL(platform string) string {
switch platform {
case "openai":
return "https://api.openai.com"
case "anthropic":
return "https://api.anthropic.com"
default:
return ""
}
}