feat: 官网 + 管理端 + Gin/GORM 后端首个完整版本 #1
@@ -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())
|
||||
}
|
||||
@@ -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 链接指向这里)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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<ReleaseInfo> | null = null
|
||||
|
||||
function load(): Promise<ReleaseInfo> {
|
||||
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<ReleaseInfo>(cache ?? STATIC_FALLBACK)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
load().then((r) => {
|
||||
if (!cancelled) setRelease(r)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
return release
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export function Hero() {
|
||||
下载桌面版 <Download className="size-4" />
|
||||
</Link>
|
||||
<a
|
||||
href={SITE.github}
|
||||
href={SITE.repo}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center rounded-lg border border-hairline-strong px-6 py-2.5 font-mono text-[14px] transition-colors hover:border-accent"
|
||||
|
||||
@@ -24,7 +24,7 @@ export function Quickstart() {
|
||||
<span className="text-white/35"># 内嵌 NATS,零配置体验</span>
|
||||
{'\n'}
|
||||
<span className="text-[#22d3ee]">$</span> git clone
|
||||
https://github.com/sundynix/agentix{'\n'}
|
||||
https://git.sundynix.cn/sundynix/sundynix-agentix{'\n'}
|
||||
<span className="text-[#22d3ee]">$</span> make demo{'\n'}
|
||||
{'\n'}
|
||||
<span className="text-white/35"># 或一条命令私有化部署</span>
|
||||
|
||||
Reference in New Issue
Block a user