From 85a0b44ddd716b64926cabc32629af2613c75bf8 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 17 Jul 2026 13:27:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20releases=20=E7=89=88=E6=9C=AC=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=20+=20=E5=85=A8=E7=AB=99=E4=BB=93=E5=BA=93=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端 /api/releases/latest 代理 gitea Releases(10min 缓存,无发布/不可达时回退 SUNDYNIX_FALLBACK_VERSION) - 前端 use-release hook 就绪(下载区动态接线暂缓) - 仓库链接统一指向 git.sundynix.cn/sundynix/sundynix-agentix,下载按钮 # 占位改为 releases 页 Co-Authored-By: Claude Opus 4.8 --- server/internal/handler/release.go | 22 ++++ server/internal/router/router.go | 2 + server/internal/service/release.go | 128 ++++++++++++++++++++++ web/src/components/layout/site-footer.tsx | 2 +- web/src/content/site.ts | 9 +- web/src/hooks/use-release.ts | 53 +++++++++ web/src/pages/home/hero.tsx | 2 +- web/src/pages/home/quickstart.tsx | 2 +- 8 files changed, 213 insertions(+), 7 deletions(-) create mode 100644 server/internal/handler/release.go create mode 100644 server/internal/service/release.go create mode 100644 web/src/hooks/use-release.ts diff --git a/server/internal/handler/release.go b/server/internal/handler/release.go new file mode 100644 index 0000000..9e652a5 --- /dev/null +++ b/server/internal/handler/release.go @@ -0,0 +1,22 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + + "git.sundynix.cn/Blizzard/sundynix-site/server/internal/pkg/resp" + "git.sundynix.cn/Blizzard/sundynix-site/server/internal/service" +) + +// ReleaseHandler 最新版本信息(代理 gitea)。 +type ReleaseHandler struct { + svc *service.ReleaseService +} + +func NewReleaseHandler() *ReleaseHandler { + return &ReleaseHandler{svc: service.NewReleaseService()} +} + +// Latest GET /api/releases/latest +func (h *ReleaseHandler) Latest(c *gin.Context) { + resp.OK(c, h.svc.Latest()) +} diff --git a/server/internal/router/router.go b/server/internal/router/router.go index 78d2009..e39d4ac 100644 --- a/server/internal/router/router.go +++ b/server/internal/router/router.go @@ -28,6 +28,8 @@ func New(db *gorm.DB) (*gin.Engine, error) { posts := handler.NewPostHandler(db) api.GET("/posts", posts.List) api.GET("/posts/:slug", posts.Get) + + api.GET("/releases/latest", handler.NewReleaseHandler().Latest) } // RSS 订阅源(页脚 RSS 链接指向这里) diff --git a/server/internal/service/release.go b/server/internal/service/release.go new file mode 100644 index 0000000..dd7d3d9 --- /dev/null +++ b/server/internal/service/release.go @@ -0,0 +1,128 @@ +// 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 +} diff --git a/web/src/components/layout/site-footer.tsx b/web/src/components/layout/site-footer.tsx index 63ece75..c6923a3 100644 --- a/web/src/components/layout/site-footer.tsx +++ b/web/src/components/layout/site-footer.tsx @@ -15,7 +15,7 @@ const COLUMNS = [ title: 'RESOURCES', links: [ { label: '博客', href: '/blog', internal: true }, - { label: 'GitHub', href: SITE.github }, + { label: '源码仓库', href: SITE.repo }, { label: 'RSS 订阅', href: '/rss.xml' }, ], }, diff --git a/web/src/content/site.ts b/web/src/content/site.ts index 390cbf5..2adaf30 100644 --- a/web/src/content/site.ts +++ b/web/src/content/site.ts @@ -1,9 +1,10 @@ /** 站点文案与数据,改这里即可,不用动组件 */ export const SITE = { + /** 静态回退版本号;线上以 /api/releases/latest 为准 */ version: 'v0.1.2', tagline: '事件驱动的 AI Agent 工作台', - github: 'https://github.com/sundynix/agentix', + repo: 'https://git.sundynix.cn/sundynix/sundynix-agentix', icp: '滇ICP备2025056308号-1', } @@ -118,21 +119,21 @@ export const DOWNLOADS = [ os: 'macOS', meta: 'universal · .app · v0.1.2', action: '下载 .dmg', - href: '#', + href: `${SITE.repo}/releases`, highlight: true, }, { os: 'Windows', meta: 'x64 · .exe · v0.1.2', action: '下载 .exe', - href: '#', + href: `${SITE.repo}/releases`, highlight: false, }, { os: '自托管', meta: 'docker compose · 全平台', action: '部署文档', - href: '#', + href: SITE.repo, highlight: false, }, ] diff --git a/web/src/hooks/use-release.ts b/web/src/hooks/use-release.ts new file mode 100644 index 0000000..3f8f90e --- /dev/null +++ b/web/src/hooks/use-release.ts @@ -0,0 +1,53 @@ +import { useEffect, useState } from 'react' +import { SITE } from '@/content/site' + +export interface ReleaseInfo { + version: string + page_url: string + macos_url?: string + windows_url?: string + source: 'gitea' | 'fallback' +} + +const STATIC_FALLBACK: ReleaseInfo = { + version: SITE.version, + page_url: `${SITE.repo}/releases`, + source: 'fallback', +} + +// 模块级缓存:整个会话只请求一次,多处组件共享 +let cache: ReleaseInfo | null = null +let inflight: Promise | null = null + +function load(): Promise { + if (cache) return Promise.resolve(cache) + inflight ??= fetch('/api/releases/latest') + .then(async (res) => { + const body = (await res.json()) as { code: number; data: ReleaseInfo } + if (!res.ok || body.code !== 0) throw new Error('release api failed') + cache = body.data + return cache + }) + .catch(() => { + cache = STATIC_FALLBACK + return cache + }) + return inflight +} + +/** 最新版本信息;请求完成前先给静态回退值,避免闪空 */ +export function useRelease(): ReleaseInfo { + const [release, setRelease] = useState(cache ?? STATIC_FALLBACK) + + useEffect(() => { + let cancelled = false + load().then((r) => { + if (!cancelled) setRelease(r) + }) + return () => { + cancelled = true + } + }, []) + + return release +} diff --git a/web/src/pages/home/hero.tsx b/web/src/pages/home/hero.tsx index c353108..86ee401 100644 --- a/web/src/pages/home/hero.tsx +++ b/web/src/pages/home/hero.tsx @@ -40,7 +40,7 @@ export function Hero() { 下载桌面版 # 内嵌 NATS,零配置体验 {'\n'} $ git clone - https://github.com/sundynix/agentix{'\n'} + https://git.sundynix.cn/sundynix/sundynix-agentix{'\n'} $ make demo{'\n'} {'\n'} # 或一条命令私有化部署