chore: sync local project state

This commit is contained in:
Your Name
2026-05-12 18:49:52 +08:00
parent afdbea6fb5
commit 1c0084afe8
105 changed files with 13221 additions and 420 deletions

View File

@@ -8,22 +8,28 @@ import (
"strings"
"time"
"github.com/prometheus/client_golang/prometheus/promhttp"
"supply-intelligence/internal/admission"
"supply-intelligence/internal/discovery"
"supply-intelligence/internal/domain"
"supply-intelligence/internal/gatewayconsumer"
"supply-intelligence/internal/poller"
"supply-intelligence/internal/probe"
"supply-intelligence/internal/publish"
"supply-intelligence/internal/repository"
)
type Server struct {
repo *repository.MemoryRepository
repo repository.Repository
probeService *probe.Service
publishService *publish.Service
gatewayConsumerService *gatewayconsumer.Service
gatewayRuntime *poller.Runtime
discoveryService *discovery.Service
admissionService *admission.Service
discoveryScheduler *discovery.DiscoveryScheduler
dashboardHandler *DashboardHandler
}
type packageChangesResponse struct {
@@ -35,13 +41,14 @@ type discoveryCandidatesResponse struct {
Items []domain.DiscoveryCandidate `json:"items"`
}
func NewServer(repo *repository.MemoryRepository, probeService *probe.Service, publishService *publish.Service, gatewayConsumerService *gatewayconsumer.Service, discoveryService *discovery.Service, admissionService *admission.Service) *Server {
return &Server{repo: repo, probeService: probeService, publishService: publishService, gatewayConsumerService: gatewayConsumerService, discoveryService: discoveryService, admissionService: admissionService}
func NewServer(repo repository.Repository, probeService *probe.Service, publishService *publish.Service, gatewayConsumerService *gatewayconsumer.Service, gatewayRuntime *poller.Runtime, discoveryService *discovery.Service, admissionService *admission.Service, discoveryScheduler *discovery.DiscoveryScheduler, dashboardHandler *DashboardHandler) *Server {
return &Server{repo: repo, probeService: probeService, publishService: publishService, gatewayConsumerService: gatewayConsumerService, gatewayRuntime: gatewayRuntime, discoveryService: discoveryService, admissionService: admissionService, discoveryScheduler: discoveryScheduler, dashboardHandler: dashboardHandler}
}
func (s *Server) Routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", s.handleHealth)
mux.Handle("/metrics", promhttp.Handler())
mux.HandleFunc("/internal/supply-intelligence/accounts/", s.handleGetRoutingState)
mux.HandleFunc("/internal/supply-intelligence/probe/evaluate", s.handleEvaluateProbe)
mux.HandleFunc("/internal/supply-intelligence/publish/package-event", s.handlePublishPackageEvent)
@@ -49,8 +56,24 @@ func (s *Server) Routes() http.Handler {
mux.HandleFunc("/internal/supply-intelligence/gateway/package-changes", s.handleListPackageChanges)
mux.HandleFunc("/internal/supply-intelligence/gateway/package-changes/", s.handleAckPackageChange)
mux.HandleFunc("/internal/supply-intelligence/gateway/consume-once", s.handleConsumeOnce)
mux.HandleFunc("/internal/supply-intelligence/gateway/runtime-status", s.handleGatewayRuntimeStatus)
mux.HandleFunc("/internal/supply-intelligence/gateway/runtime/pause", s.handleGatewayRuntimePause)
mux.HandleFunc("/internal/supply-intelligence/gateway/runtime/resume", s.handleGatewayRuntimeResume)
mux.HandleFunc("/internal/supply-intelligence/admission/run", s.handleAdmissionRun)
mux.HandleFunc("/internal/supply-intelligence/admission/candidates", s.handleAdmissionCandidates)
mux.HandleFunc("/internal/supply-intelligence/models/", s.handleModelAdmissionState)
// Dashboard endpoints
if s.dashboardHandler != nil {
mux.HandleFunc("/internal/supply-intelligence/dashboard/accounts", s.dashboardHandler.ListAccounts)
mux.HandleFunc("/internal/supply-intelligence/dashboard/accounts/", s.dashboardHandler.GetProbeHistory)
mux.HandleFunc("/internal/supply-intelligence/dashboard/models", s.dashboardHandler.ListModels)
mux.HandleFunc("/internal/supply-intelligence/dashboard/candidates", s.dashboardHandler.ListCandidates)
}
// Discovery scan endpoints
if s.discoveryScheduler != nil {
mux.HandleFunc("/internal/supply-intelligence/discovery/scan", s.handleDiscoveryScan)
mux.HandleFunc("/internal/supply-intelligence/discovery/scan-platform", s.handleDiscoveryScanPlatform)
}
return mux
}
@@ -75,7 +98,7 @@ func (s *Server) handleGetRoutingState(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid_account_id"})
return
}
state, ok := s.repo.GetRoutingState(accountID)
state, ok := s.repo.GetRoutingState(r.Context(), accountID)
if !ok {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not_found"})
return
@@ -148,10 +171,8 @@ func (s *Server) handlePublishPackageEvent(w http.ResponseWriter, r *http.Reques
var payload struct {
EventID string `json:"event_id"`
PackageID int64 `json:"package_id"`
Platform string `json:"platform"`
Model string `json:"model"`
Version int64 `json:"version"`
OccurredAt string `json:"occurred_at"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
@@ -169,23 +190,30 @@ func (s *Server) handlePublishPackageEvent(w http.ResponseWriter, r *http.Reques
occurredAt = parsed
}
event, err := s.publishService.RecordPackagePublished(r.Context(), publish.RecordPackagePublishedInput{
out, err := s.publishService.PublishDraft(r.Context(), publish.PublishDraftInput{
EventID: payload.EventID,
PackageID: payload.PackageID,
Platform: payload.Platform,
Model: payload.Model,
Version: payload.Version,
OccurredAt: occurredAt,
})
if err != nil {
if errors.Is(err, publish.ErrInvalidPublishInput) {
switch {
case errors.Is(err, publish.ErrInvalidPublishInput):
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid_publish_input"})
return
case errors.Is(err, publish.ErrCandidateOrPackageMissing):
writeJSON(w, http.StatusNotFound, map[string]string{"error": "candidate_or_package_missing"})
case errors.Is(err, publish.ErrDuplicatePublishRequest):
writeJSON(w, http.StatusConflict, map[string]string{"error": "duplicate_publish_request"})
case errors.Is(err, publish.ErrPackageAlreadyPublished):
writeJSON(w, http.StatusConflict, map[string]string{"error": "publish_already_applied"})
case errors.Is(err, publish.ErrCandidateNotPublishable), errors.Is(err, publish.ErrPackageNotPublishable):
writeJSON(w, http.StatusConflict, map[string]string{"error": "publish_precondition_failed"})
default:
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal_error"})
}
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal_error"})
return
}
writeJSON(w, http.StatusOK, event)
writeJSON(w, http.StatusOK, out)
}
func (s *Server) handleDiscoveryCandidates(w http.ResponseWriter, r *http.Request) {
@@ -265,7 +293,15 @@ func parseDiscoveryCandidateStatus(raw string) (domain.DiscoveryCandidateStatus,
}
status := domain.DiscoveryCandidateStatus(raw)
switch status {
case domain.DiscoveryCandidateStatusPendingAdmission, domain.DiscoveryCandidateStatusAdmitted, domain.DiscoveryCandidateStatusRejected:
case domain.DiscoveryCandidateStatusDiscovered,
domain.DiscoveryCandidateStatusTesting,
domain.DiscoveryCandidateStatusTestPassed,
domain.DiscoveryCandidateStatusTestFailed,
domain.DiscoveryCandidateStatusRetryPending,
domain.DiscoveryCandidateStatusIgnored,
domain.DiscoveryCandidateStatusPublished,
domain.DiscoveryCandidateStatusDeprecated,
domain.DiscoveryCandidateStatusClosed:
return status, true
default:
return "", false
@@ -277,7 +313,7 @@ func (s *Server) handleListPackageChanges(w http.ResponseWriter, r *http.Request
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
return
}
items, nextCursor := s.repo.ListPackageEventsAfter(strings.TrimSpace(r.URL.Query().Get("cursor")))
items, nextCursor := s.repo.ListPackageEventsAfter(r.Context(), strings.TrimSpace(r.URL.Query().Get("cursor")))
writeJSON(w, http.StatusOK, packageChangesResponse{Items: items, NextCursor: nextCursor})
}
@@ -311,7 +347,7 @@ func (s *Server) handleAckPackageChange(w http.ResponseWriter, r *http.Request)
if consumer == "" {
consumer = "gateway"
}
_, err := s.repo.AckPackageEvent(eventID, consumer, ackResult, payload.Detail, time.Now().UTC())
_, err := s.repo.AckPackageEvent(r.Context(), eventID, consumer, ackResult, payload.Detail, time.Now().UTC())
if err != nil {
if errors.Is(err, repository.ErrEventNotFound) {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not_found"})
@@ -350,6 +386,64 @@ func (s *Server) handleConsumeOnce(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, out)
}
func (s *Server) handleGatewayRuntimeStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
return
}
if s.gatewayRuntime == nil || s.repo == nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "gateway_runtime_unavailable"})
return
}
now := time.Now().UTC()
status := s.gatewayRuntime.Status()
consumer := strings.TrimSpace(r.URL.Query().Get("consumer"))
if consumer == "" {
consumer = "gateway"
}
writeJSON(w, http.StatusOK, map[string]any{
"started": status.Started,
"paused": status.Paused,
"cursor": status.Cursor,
"last_poll_at": status.LastPollAt,
"last_error": status.LastError,
"pending_retry_events": s.repo.CountRetryablePendingPackageEvents(r.Context(), consumer, now),
"failed_events": s.repo.CountPackageEventsBySyncStatus(r.Context(), domain.GatewaySyncStatusFailed),
})
}
func (s *Server) handleGatewayRuntimePause(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
return
}
if s.gatewayRuntime == nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "gateway_runtime_unavailable"})
return
}
if !s.gatewayRuntime.Pause() {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "pause_failed"})
return
}
writeJSON(w, http.StatusOK, map[string]bool{"paused": true})
}
func (s *Server) handleGatewayRuntimeResume(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
return
}
if s.gatewayRuntime == nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "gateway_runtime_unavailable"})
return
}
if !s.gatewayRuntime.Resume() {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "resume_failed"})
return
}
writeJSON(w, http.StatusOK, map[string]bool{"paused": false})
}
func writeJSON(w http.ResponseWriter, status int, body any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
@@ -395,7 +489,7 @@ func (s *Server) handleAdmissionRun(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, result)
}
// handleAdmissionCandidates lists candidates pending admission testing
// handleAdmissionCandidates lists candidates currently runnable for admission testing
func (s *Server) handleAdmissionCandidates(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
@@ -410,6 +504,138 @@ func (s *Server) handleAdmissionCandidates(w http.ResponseWriter, r *http.Reques
writeJSON(w, http.StatusOK, map[string]any{"items": candidates})
}
func (s *Server) handleModelAdmissionState(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
return
}
prefix := "/internal/supply-intelligence/models/"
path := strings.TrimPrefix(r.URL.Path, prefix)
parts := strings.Split(path, "/")
if len(parts) != 3 || parts[2] != "admission-state" {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not_found"})
return
}
platform := strings.TrimSpace(parts[0])
model := strings.TrimSpace(parts[1])
if platform == "" || model == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid_model_path"})
return
}
var candidate *domain.DiscoveryCandidate
if latest, ok := s.repo.GetLatestDiscoveryCandidateContext(r.Context(), platform, model); ok {
copyCandidate := latest
candidate = &copyCandidate
}
pkg, hasPackage := s.repo.GetSupplyPackage(r.Context(), platform, model)
var lastEvent *domain.PackageChangeEvent
if hasPackage {
if latestEvent, ok := s.repo.GetLatestPackageEvent(r.Context(), platform, model); ok {
copyEvt := latestEvent
lastEvent = &copyEvt
}
}
gatewaySyncStatus := domain.GatewaySyncStatus("")
if lastEvent != nil {
gatewaySyncStatus = lastEvent.GatewaySyncStatus
}
writeJSON(w, http.StatusOK, map[string]any{
"platform": platform,
"model": model,
"candidate": candidate,
"package": packageOrNil(hasPackage, pkg),
"gateway_sync_status": gatewaySyncStatus,
"last_event": lastEvent,
})
}
func packageOrNil(ok bool, pkg domain.SupplyPackage) any {
if !ok {
return nil
}
return pkg
}
func domainAccountStatus(raw string) domain.AccountStatus {
return domain.AccountStatus(raw)
}
// handleDiscoveryScan runs discovery across all registered platforms.
// POST /internal/supply-intelligence/discovery/scan
func (s *Server) handleDiscoveryScan(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
return
}
if s.discoveryScheduler == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "discovery_scheduler_unavailable"})
return
}
results, err := s.discoveryScheduler.ScanAllPlatforms(r.Context())
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
type scanResultRow struct {
Platform string `json:"platform"`
NewModels int `json:"new_models"`
RemovedModels []string `json:"removed_models,omitempty"`
Errors []string `json:"errors,omitempty"`
}
rows := make([]scanResultRow, 0, len(results))
for _, r := range results {
rows = append(rows, scanResultRow{
Platform: r.Platform,
NewModels: r.NewModels,
RemovedModels: r.RemovedModels,
Errors: r.Errors,
})
}
writeJSON(w, http.StatusOK, map[string]any{"results": rows, "total_platforms": len(results)})
}
// handleDiscoveryScanPlatform runs discovery for a single platform.
// POST /internal/supply-intelligence/discovery/scan-platform
// Body: {"platform": "openai"}
func (s *Server) handleDiscoveryScanPlatform(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
return
}
if s.discoveryScheduler == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "discovery_scheduler_unavailable"})
return
}
var payload struct {
Platform string `json:"platform"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid_json"})
return
}
if strings.TrimSpace(payload.Platform) == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing_platform"})
return
}
result, err := s.discoveryScheduler.ScanPlatform(r.Context(), payload.Platform)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]any{
"platform": result.Platform,
"new_models": result.NewModels,
"removed_models": result.RemovedModels,
"errors": result.Errors,
})
}