// Package service 业务服务层。 package service import ( "encoding/json" "fmt" "net/http" "os" "strings" "sync" "time" ) // Release 对外暴露的最新版本信息。 type Release struct { Version string `json:"version"` PageURL string `json:"page_url"` MacOS string `json:"macos_url,omitempty"` Windows string `json:"windows_url,omitempty"` Source string `json:"source"` // gitea | fallback } // ReleaseService 代理 gitea Releases API,带缓存与静态回退。 type ReleaseService struct { giteaBase string repo string fallback Release mu sync.Mutex cached *Release cachedAt time.Time ttl time.Duration client *http.Client } func NewReleaseService() *ReleaseService { base := envOr("SUNDYNIX_GITEA_BASE", "https://git.sundynix.cn") repo := envOr("SUNDYNIX_RELEASE_REPO", "sundynix/sundynix-agentix") pageURL := fmt.Sprintf("%s/%s/releases", base, repo) return &ReleaseService{ giteaBase: base, repo: repo, fallback: Release{ Version: envOr("SUNDYNIX_FALLBACK_VERSION", "v0.1.2"), PageURL: pageURL, Source: "fallback", }, ttl: 10 * time.Minute, client: &http.Client{Timeout: 8 * time.Second}, } } // Latest 返回最新 release;gitea 不可达或无发布时回退静态配置。 func (s *ReleaseService) Latest() Release { s.mu.Lock() defer s.mu.Unlock() if s.cached != nil && time.Since(s.cachedAt) < s.ttl { return *s.cached } rel := s.fetch() s.cached = &rel s.cachedAt = time.Now() return rel } type giteaAsset struct { Name string `json:"name"` URL string `json:"browser_download_url"` } type giteaRelease struct { TagName string `json:"tag_name"` HTMLURL string `json:"html_url"` Draft bool `json:"draft"` Prerelease bool `json:"prerelease"` Assets []giteaAsset `json:"assets"` } func (s *ReleaseService) fetch() Release { url := fmt.Sprintf("%s/api/v1/repos/%s/releases?limit=5", s.giteaBase, s.repo) resp, err := s.client.Get(url) if err != nil { return s.fallback } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return s.fallback } var list []giteaRelease if err := json.NewDecoder(resp.Body).Decode(&list); err != nil { return s.fallback } for _, r := range list { if r.Draft || r.Prerelease { continue } rel := Release{ Version: r.TagName, PageURL: r.HTMLURL, Source: "gitea", } if rel.PageURL == "" { rel.PageURL = s.fallback.PageURL } for _, a := range r.Assets { name := strings.ToLower(a.Name) switch { case strings.HasSuffix(name, ".dmg"), strings.Contains(name, "darwin"), strings.Contains(name, "macos"): rel.MacOS = a.URL case strings.HasSuffix(name, ".exe"), strings.Contains(name, "windows"): rel.Windows = a.URL } } return rel } return s.fallback } func envOr(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback }