From 2ee9b5ffa2ee2d6fbdea149bfb84830fdd256bee Mon Sep 17 00:00:00 2001 From: yyhuni Date: Sat, 10 Jan 2026 10:27:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/apps/common/urls.py | 3 + backend/apps/common/views/__init__.py | 3 + backend/apps/common/views/version_views.py | 121 +++++++++++++ frontend/components/about-dialog.tsx | 189 +++++++++++++++++++++ frontend/components/app-sidebar.tsx | 36 ++-- frontend/hooks/use-version.ts | 19 +++ frontend/messages/en.json | 19 ++- frontend/messages/zh.json | 19 ++- frontend/services/version.service.ts | 14 ++ frontend/types/version.types.ts | 13 ++ 10 files changed, 413 insertions(+), 23 deletions(-) create mode 100644 backend/apps/common/views/version_views.py create mode 100644 frontend/components/about-dialog.tsx create mode 100644 frontend/hooks/use-version.ts create mode 100644 frontend/services/version.service.ts create mode 100644 frontend/types/version.types.ts diff --git a/backend/apps/common/urls.py b/backend/apps/common/urls.py index ef8c7434..a8e4c0cf 100644 --- a/backend/apps/common/urls.py +++ b/backend/apps/common/urls.py @@ -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'), diff --git a/backend/apps/common/views/__init__.py b/backend/apps/common/views/__init__.py index 2397f118..e937d7ac 100644 --- a/backend/apps/common/views/__init__.py +++ b/backend/apps/common/views/__init__.py @@ -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', ] diff --git a/backend/apps/common/views/version_views.py b/backend/apps/common/views/version_views.py new file mode 100644 index 00000000..44390676 --- /dev/null +++ b/backend/apps/common/views/version_views.py @@ -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,请稍后重试", + ) diff --git a/frontend/components/about-dialog.tsx b/frontend/components/about-dialog.tsx new file mode 100644 index 00000000..1c8a1fdb --- /dev/null +++ b/frontend/components/about-dialog.tsx @@ -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(null) + const [checkError, setCheckError] = useState(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 ( + + + {children} + + + + {t('title')} + + +
+ {/* Logo and name */} +
+
+ +
+

XingRin

+

{t('description')}

+
+ + {/* Version info */} +
+
+ {t('currentVersion')} + {currentVersion} +
+ + {updateResult && ( +
+ {t('latestVersion')} +
+ {latestVersion} + {hasUpdate ? ( + + + {t('updateAvailable')} + + ) : ( + + + {t('upToDate')} + + )} +
+
+ )} + + {checkError && ( +

{checkError}

+ )} + +
+ + + {hasUpdate && updateResult?.releaseUrl && ( + + )} +
+ + {hasUpdate && ( +
+

{t('updateHint')}

+ + sudo ./update.sh + +
+ )} +
+ + + + {/* Links */} + + + {/* Footer */} +

+ © 2025 XingRin · MIT License +

+
+
+
+ ) +} diff --git a/frontend/components/app-sidebar.tsx b/frontend/components/app-sidebar.tsx index 942b444e..ea02479b 100644 --- a/frontend/components/app-sidebar.tsx +++ b/frontend/components/app-sidebar.tsx @@ -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) { }, ] - // 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) { {/* System settings navigation menu */} - {/* Secondary navigation menu, using mt-auto to push to bottom */} - + {/* About system button */} + + + + + + + + {t('about')} + + + + + + {/* Sidebar footer */} diff --git a/frontend/hooks/use-version.ts b/frontend/hooks/use-version.ts new file mode 100644 index 00000000..5dcce7a1 --- /dev/null +++ b/frontend/hooks/use-version.ts @@ -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 分钟缓存 + }) +} diff --git a/frontend/messages/en.json b/frontend/messages/en.json index c4e250ad..d721ffee 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -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" } } diff --git a/frontend/messages/zh.json b/frontend/messages/zh.json index f8cc97a3..5ffa6b43 100644 --- a/frontend/messages/zh.json +++ b/frontend/messages/zh.json @@ -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": "使用文档" } } diff --git a/frontend/services/version.service.ts b/frontend/services/version.service.ts new file mode 100644 index 00000000..2cc1c881 --- /dev/null +++ b/frontend/services/version.service.ts @@ -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 { + const res = await api.get('/system/version/') + return res.data + } + + static async checkUpdate(): Promise { + const res = await api.get('/system/check-update/') + return res.data + } +} diff --git a/frontend/types/version.types.ts b/frontend/types/version.types.ts new file mode 100644 index 00000000..ffb69667 --- /dev/null +++ b/frontend/types/version.types.ts @@ -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 +}