更新版本

This commit is contained in:
yyhuni
2026-01-10 10:27:48 +08:00
parent 648a1888d4
commit 2ee9b5ffa2
10 changed files with 413 additions and 23 deletions

View File

@@ -14,6 +14,7 @@ from .views import (
LoginView, LogoutView, MeView, ChangePasswordView,
SystemLogsView, SystemLogFilesView, HealthCheckView,
GlobalBlacklistView,
VersionView, CheckUpdateView,
)
urlpatterns = [
@@ -29,6 +30,8 @@ urlpatterns = [
# 系统管理
path('system/logs/', SystemLogsView.as_view(), name='system-logs'),
path('system/logs/files/', SystemLogFilesView.as_view(), name='system-log-files'),
path('system/version/', VersionView.as_view(), name='system-version'),
path('system/check-update/', CheckUpdateView.as_view(), name='system-check-update'),
# 黑名单管理PUT 全量替换模式)
path('blacklist/rules/', GlobalBlacklistView.as_view(), name='blacklist-rules'),

View File

@@ -6,16 +6,19 @@
- 认证相关视图:登录、登出、用户信息、修改密码
- 系统日志视图:实时日志查看
- 黑名单视图:全局黑名单规则管理
- 版本视图:系统版本和更新检查
"""
from .health_views import HealthCheckView
from .auth_views import LoginView, LogoutView, MeView, ChangePasswordView
from .system_log_views import SystemLogsView, SystemLogFilesView
from .blacklist_views import GlobalBlacklistView
from .version_views import VersionView, CheckUpdateView
__all__ = [
'HealthCheckView',
'LoginView', 'LogoutView', 'MeView', 'ChangePasswordView',
'SystemLogsView', 'SystemLogFilesView',
'GlobalBlacklistView',
'VersionView', 'CheckUpdateView',
]

View File

@@ -0,0 +1,121 @@
"""
系统版本相关视图
"""
import logging
from pathlib import Path
import requests
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from apps.common.error_codes import ErrorCodes
from apps.common.response_helpers import error_response, success_response
logger = logging.getLogger(__name__)
# GitHub 仓库信息
GITHUB_REPO = "yyhuni/xingrin"
GITHUB_API_URL = f"https://api.github.com/repos/{GITHUB_REPO}/releases/latest"
GITHUB_RELEASES_URL = f"https://github.com/{GITHUB_REPO}/releases"
def get_current_version() -> str:
"""读取当前版本号"""
version_file = Path(__file__).parent.parent.parent.parent.parent / 'VERSION'
try:
return version_file.read_text(encoding='utf-8').strip()
except FileNotFoundError:
return "unknown"
def compare_versions(current: str, latest: str) -> bool:
"""
比较版本号,判断是否有更新
Returns:
True 表示有更新可用
"""
def parse_version(v: str) -> tuple:
v = v.lstrip('v')
parts = v.split('.')
result = []
for part in parts:
if '-' in part:
num, _ = part.split('-', 1)
result.append(int(num))
else:
result.append(int(part))
return tuple(result)
try:
return parse_version(latest) > parse_version(current)
except (ValueError, AttributeError):
return False
class VersionView(APIView):
"""获取当前系统版本"""
def get(self, _request: Request) -> Response:
"""获取当前版本信息"""
return success_response(data={
'version': get_current_version(),
'github_repo': GITHUB_REPO,
})
class CheckUpdateView(APIView):
"""检查系统更新"""
def get(self, _request: Request) -> Response:
"""
检查是否有新版本
Returns:
- current_version: 当前版本
- latest_version: 最新版本
- has_update: 是否有更新
- release_url: 发布页面 URL
- release_notes: 更新说明(如果有)
"""
current_version = get_current_version()
try:
response = requests.get(
GITHUB_API_URL,
headers={'Accept': 'application/vnd.github.v3+json'},
timeout=10
)
if response.status_code == 404:
return success_response(data={
'current_version': current_version,
'latest_version': current_version,
'has_update': False,
'release_url': GITHUB_RELEASES_URL,
'release_notes': None,
})
response.raise_for_status()
release_data = response.json()
latest_version = release_data.get('tag_name', current_version)
has_update = compare_versions(current_version, latest_version)
return success_response(data={
'current_version': current_version,
'latest_version': latest_version,
'has_update': has_update,
'release_url': release_data.get('html_url', GITHUB_RELEASES_URL),
'release_notes': release_data.get('body'),
'published_at': release_data.get('published_at'),
})
except requests.RequestException as e:
logger.warning("检查更新失败: %s", e)
return error_response(
code=ErrorCodes.SERVER_ERROR,
message="无法连接到 GitHub请稍后重试",
)

View File

@@ -0,0 +1,189 @@
"use client"
import { useState } from 'react'
import { useTranslations } from 'next-intl'
import { useQueryClient } from '@tanstack/react-query'
import {
IconRadar,
IconRefresh,
IconExternalLink,
IconBrandGithub,
IconMessageReport,
IconBook,
IconFileText,
IconCheck,
IconArrowUp,
} from '@tabler/icons-react'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { Badge } from '@/components/ui/badge'
import { useVersion } from '@/hooks/use-version'
import { VersionService } from '@/services/version.service'
import type { UpdateCheckResult } from '@/types/version.types'
interface AboutDialogProps {
children: React.ReactNode
}
export function AboutDialog({ children }: AboutDialogProps) {
const t = useTranslations('about')
const { data: versionData } = useVersion()
const queryClient = useQueryClient()
const [isChecking, setIsChecking] = useState(false)
const [updateResult, setUpdateResult] = useState<UpdateCheckResult | null>(null)
const [checkError, setCheckError] = useState<string | null>(null)
const handleCheckUpdate = async () => {
setIsChecking(true)
setCheckError(null)
try {
const result = await VersionService.checkUpdate()
setUpdateResult(result)
queryClient.setQueryData(['check-update'], result)
} catch {
setCheckError(t('checkFailed'))
} finally {
setIsChecking(false)
}
}
const currentVersion = updateResult?.currentVersion || versionData?.version || '-'
const latestVersion = updateResult?.latestVersion
const hasUpdate = updateResult?.hasUpdate
return (
<Dialog>
<DialogTrigger asChild>
{children}
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{t('title')}</DialogTitle>
</DialogHeader>
<div className="space-y-6">
{/* Logo and name */}
<div className="flex flex-col items-center py-4">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/10 mb-3">
<IconRadar className="h-8 w-8 text-primary" />
</div>
<h2 className="text-xl font-semibold">XingRin</h2>
<p className="text-sm text-muted-foreground">{t('description')}</p>
</div>
{/* Version info */}
<div className="rounded-lg border p-4 space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">{t('currentVersion')}</span>
<span className="font-mono text-sm">{currentVersion}</span>
</div>
{updateResult && (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">{t('latestVersion')}</span>
<div className="flex items-center gap-2">
<span className="font-mono text-sm">{latestVersion}</span>
{hasUpdate ? (
<Badge variant="default" className="gap-1">
<IconArrowUp className="h-3 w-3" />
{t('updateAvailable')}
</Badge>
) : (
<Badge variant="secondary" className="gap-1">
<IconCheck className="h-3 w-3" />
{t('upToDate')}
</Badge>
)}
</div>
</div>
)}
{checkError && (
<p className="text-sm text-destructive">{checkError}</p>
)}
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={handleCheckUpdate}
disabled={isChecking}
>
<IconRefresh className={`h-4 w-4 mr-2 ${isChecking ? 'animate-spin' : ''}`} />
{isChecking ? t('checking') : t('checkUpdate')}
</Button>
{hasUpdate && updateResult?.releaseUrl && (
<Button
variant="default"
size="sm"
className="flex-1"
asChild
>
<a href={updateResult.releaseUrl} target="_blank" rel="noopener noreferrer">
<IconExternalLink className="h-4 w-4 mr-2" />
{t('viewRelease')}
</a>
</Button>
)}
</div>
{hasUpdate && (
<div className="rounded-md bg-muted p-3 text-sm text-muted-foreground">
<p>{t('updateHint')}</p>
<code className="mt-1 block rounded bg-background px-2 py-1 font-mono text-xs">
sudo ./update.sh
</code>
</div>
)}
</div>
<Separator />
{/* Links */}
<div className="grid grid-cols-2 gap-2">
<Button variant="ghost" size="sm" className="justify-start" asChild>
<a href="https://github.com/yyhuni/xingrin" target="_blank" rel="noopener noreferrer">
<IconBrandGithub className="h-4 w-4 mr-2" />
GitHub
</a>
</Button>
<Button variant="ghost" size="sm" className="justify-start" asChild>
<a href="https://github.com/yyhuni/xingrin/releases" target="_blank" rel="noopener noreferrer">
<IconFileText className="h-4 w-4 mr-2" />
{t('changelog')}
</a>
</Button>
<Button variant="ghost" size="sm" className="justify-start" asChild>
<a href="https://github.com/yyhuni/xingrin/issues" target="_blank" rel="noopener noreferrer">
<IconMessageReport className="h-4 w-4 mr-2" />
{t('feedback')}
</a>
</Button>
<Button variant="ghost" size="sm" className="justify-start" asChild>
<a href="https://github.com/yyhuni/xingrin#readme" target="_blank" rel="noopener noreferrer">
<IconBook className="h-4 w-4 mr-2" />
{t('docs')}
</a>
</Button>
</div>
{/* Footer */}
<p className="text-center text-xs text-muted-foreground">
© 2025 XingRin · MIT License
</p>
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -5,7 +5,6 @@ import type * as React from "react"
// Import various icons from Tabler Icons library
import {
IconDashboard, // Dashboard icon
IconHelp, // Help icon
IconListDetails, // List details icon
IconSettings, // Settings icon
IconUsers, // Users icon
@@ -15,10 +14,10 @@ import {
IconServer, // Server icon
IconTerminal2, // Terminal icon
IconBug, // Vulnerability icon
IconMessageReport, // Feedback icon
IconSearch, // Search icon
IconKey, // API Key icon
IconBan, // Blacklist icon
IconInfoCircle, // About icon
} from "@tabler/icons-react"
// Import internationalization hook
import { useTranslations } from 'next-intl'
@@ -27,8 +26,8 @@ import { Link, usePathname } from '@/i18n/navigation'
// Import custom navigation components
import { NavSystem } from "@/components/nav-system"
import { NavSecondary } from "@/components/nav-secondary"
import { NavUser } from "@/components/nav-user"
import { AboutDialog } from "@/components/about-dialog"
// Import sidebar UI components
import {
Sidebar,
@@ -139,20 +138,6 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
},
]
// Secondary navigation menu items
const navSecondary = [
{
title: t('feedback'),
url: "https://github.com/yyhuni/xingrin/issues",
icon: IconMessageReport,
},
{
title: t('help'),
url: "https://github.com/yyhuni/xingrin",
icon: IconHelp,
},
]
// System settings related menu items
const documents = [
{
@@ -271,8 +256,21 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
{/* System settings navigation menu */}
<NavSystem items={documents} />
{/* Secondary navigation menu, using mt-auto to push to bottom */}
<NavSecondary items={navSecondary} className="mt-auto" />
{/* About system button */}
<SidebarGroup className="mt-auto">
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<AboutDialog>
<SidebarMenuButton>
<IconInfoCircle />
<span>{t('about')}</span>
</SidebarMenuButton>
</AboutDialog>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
{/* Sidebar footer */}

View File

@@ -0,0 +1,19 @@
import { useQuery } from '@tanstack/react-query'
import { VersionService } from '@/services/version.service'
export function useVersion() {
return useQuery({
queryKey: ['version'],
queryFn: () => VersionService.getVersion(),
staleTime: Infinity,
})
}
export function useCheckUpdate() {
return useQuery({
queryKey: ['check-update'],
queryFn: () => VersionService.checkUpdate(),
enabled: false, // 手动触发
staleTime: 5 * 60 * 1000, // 5 分钟缓存
})
}

View File

@@ -325,8 +325,7 @@
"notifications": "Notifications",
"apiKeys": "API Keys",
"globalBlacklist": "Global Blacklist",
"help": "Get Help",
"feedback": "Feedback"
"about": "About"
},
"search": {
"title": "Asset Search",
@@ -2292,5 +2291,21 @@
"conflict": "Resource conflict, please check and try again",
"unauthorized": "Please login first",
"rateLimited": "Too many requests, please try again later"
},
"about": {
"title": "About XingRin",
"description": "Attack Surface Management Platform",
"currentVersion": "Current Version",
"latestVersion": "Latest Version",
"checkUpdate": "Check Update",
"checking": "Checking...",
"checkFailed": "Failed to check update, please try again later",
"updateAvailable": "Update Available",
"upToDate": "Up to Date",
"viewRelease": "View Release",
"updateHint": "Run the following command in project root to update:",
"changelog": "Changelog",
"feedback": "Feedback",
"docs": "Documentation"
}
}

View File

@@ -325,8 +325,7 @@
"notifications": "通知设置",
"apiKeys": "API 密钥",
"globalBlacklist": "全局黑名单",
"help": "获取帮助",
"feedback": "反馈建议"
"about": "关于系统"
},
"search": {
"title": "资产搜索",
@@ -2292,5 +2291,21 @@
"conflict": "资源冲突,请检查后重试",
"unauthorized": "请先登录",
"rateLimited": "请求过于频繁,请稍后重试"
},
"about": {
"title": "关于 XingRin",
"description": "攻击面管理平台",
"currentVersion": "当前版本",
"latestVersion": "最新版本",
"checkUpdate": "检查更新",
"checking": "检查中...",
"checkFailed": "检查更新失败,请稍后重试",
"updateAvailable": "有更新",
"upToDate": "已是最新",
"viewRelease": "查看发布",
"updateHint": "在项目根目录运行以下命令更新:",
"changelog": "更新日志",
"feedback": "问题反馈",
"docs": "使用文档"
}
}

View File

@@ -0,0 +1,14 @@
import { api } from '@/lib/api-client'
import type { VersionInfo, UpdateCheckResult } from '@/types/version.types'
export class VersionService {
static async getVersion(): Promise<VersionInfo> {
const res = await api.get<VersionInfo>('/system/version/')
return res.data
}
static async checkUpdate(): Promise<UpdateCheckResult> {
const res = await api.get<UpdateCheckResult>('/system/check-update/')
return res.data
}
}

View File

@@ -0,0 +1,13 @@
export interface VersionInfo {
version: string
githubRepo: string
}
export interface UpdateCheckResult {
currentVersion: string
latestVersion: string
hasUpdate: boolean
releaseUrl: string
releaseNotes: string | null
publishedAt: string | null
}