From 82330b8d105ca6a1dec20879c308ea501584a5b4 Mon Sep 17 00:00:00 2001 From: Soulter <37870767+Soulter@users.noreply.github.com> Date: Sat, 20 Dec 2025 16:33:12 +0800 Subject: [PATCH] feat: add changelog functionality and dialog component (#4135) * feat: add changelog functionality and dialog component - Implemented new routes for fetching changelogs and available versions in StatRoute. - Created ChangelogDialog.vue for displaying changelog content and version selection. - Updated VerticalSidebar.vue to include a button for opening the changelog dialog. - Enhanced localization files for English and Chinese to support new changelog features. - Adjusted styles in VerticalHeader.vue for improved layout consistency. * chore: ruff format --- astrbot/dashboard/routes/stat.py | 96 ++++++++ .../src/components/shared/ChangelogDialog.vue | 209 ++++++++++++++++++ .../i18n/locales/en-US/core/navigation.json | 9 + .../i18n/locales/zh-CN/core/navigation.json | 9 + .../full/vertical-header/VerticalHeader.vue | 2 +- .../full/vertical-sidebar/VerticalSidebar.vue | 17 +- 6 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 dashboard/src/components/shared/ChangelogDialog.vue diff --git a/astrbot/dashboard/routes/stat.py b/astrbot/dashboard/routes/stat.py index 8df690cc2..054eec995 100644 --- a/astrbot/dashboard/routes/stat.py +++ b/astrbot/dashboard/routes/stat.py @@ -1,6 +1,9 @@ +import os +import re import threading import time import traceback +from functools import cmp_to_key import aiohttp import psutil @@ -11,7 +14,9 @@ from astrbot.core.config import VERSION from astrbot.core.core_lifecycle import AstrBotCoreLifecycle from astrbot.core.db import BaseDatabase from astrbot.core.db.migration.helper import check_migration_needed_v4 +from astrbot.core.utils.astrbot_path import get_astrbot_path from astrbot.core.utils.io import get_dashboard_version +from astrbot.core.utils.version_comparator import VersionComparator from .route import Response, Route, RouteContext @@ -30,6 +35,8 @@ class StatRoute(Route): "/stat/start-time": ("GET", self.get_start_time), "/stat/restart-core": ("POST", self.restart_core), "/stat/test-ghproxy-connection": ("POST", self.test_ghproxy_connection), + "/stat/changelog": ("GET", self.get_changelog), + "/stat/changelog/list": ("GET", self.list_changelog_versions), } self.db_helper = db_helper self.register_routes() @@ -183,3 +190,92 @@ class StatRoute(Route): except Exception as e: logger.error(traceback.format_exc()) return Response().error(f"Error: {e!s}").__dict__ + + async def get_changelog(self): + """获取指定版本的更新日志""" + try: + version = request.args.get("version") + if not version: + return Response().error("version parameter is required").__dict__ + + version = version.lstrip("v") + + # 防止路径遍历攻击 + if not re.match(r"^[a-zA-Z0-9._-]+$", version): + return Response().error("Invalid version format").__dict__ + if ".." in version or "/" in version or "\\" in version: + return Response().error("Invalid version format").__dict__ + + filename = f"v{version}.md" + project_path = get_astrbot_path() + changelogs_dir = os.path.join(project_path, "changelogs") + changelog_path = os.path.join(changelogs_dir, filename) + + # 规范化路径,防止符号链接攻击 + changelog_path = os.path.realpath(changelog_path) + changelogs_dir = os.path.realpath(changelogs_dir) + + # 验证最终路径在预期的 changelogs 目录内(防止路径遍历) + # 确保规范化后的路径以 changelogs_dir 开头,且是目录内的文件 + changelog_path_normalized = os.path.normpath(changelog_path) + changelogs_dir_normalized = os.path.normpath(changelogs_dir) + + # 检查路径是否在预期目录内(必须是目录的子文件,不能是目录本身) + expected_prefix = changelogs_dir_normalized + os.sep + if not changelog_path_normalized.startswith(expected_prefix): + logger.warning( + f"Path traversal attempt detected: {version} -> {changelog_path}", + ) + return Response().error("Invalid version format").__dict__ + + if not os.path.exists(changelog_path): + return ( + Response() + .error(f"Changelog for version {version} not found") + .__dict__ + ) + if not os.path.isfile(changelog_path): + return ( + Response() + .error(f"Changelog for version {version} not found") + .__dict__ + ) + + with open(changelog_path, encoding="utf-8") as f: + content = f.read() + + return Response().ok({"content": content, "version": version}).__dict__ + except Exception as e: + logger.error(traceback.format_exc()) + return Response().error(f"Error: {e!s}").__dict__ + + async def list_changelog_versions(self): + """获取所有可用的更新日志版本列表""" + try: + project_path = get_astrbot_path() + changelogs_dir = os.path.join(project_path, "changelogs") + + if not os.path.exists(changelogs_dir): + return Response().ok({"versions": []}).__dict__ + + versions = [] + for filename in os.listdir(changelogs_dir): + if filename.endswith(".md") and filename.startswith("v"): + # 提取版本号(去除 v 前缀和 .md 后缀) + version = filename[1:-3] # 去掉 "v" 和 ".md" + # 验证版本号格式 + if re.match(r"^[a-zA-Z0-9._-]+$", version): + versions.append(version) + + # 按版本号排序(降序,最新的在前) + # 使用项目中的 VersionComparator 进行语义化版本号排序 + versions.sort( + key=cmp_to_key( + lambda v1, v2: VersionComparator.compare_version(v2, v1), + ), + ) + + return Response().ok({"versions": versions}).__dict__ + except Exception as e: + logger.error(traceback.format_exc()) + return Response().error(f"Error: {e!s}").__dict__ diff --git a/dashboard/src/components/shared/ChangelogDialog.vue b/dashboard/src/components/shared/ChangelogDialog.vue new file mode 100644 index 000000000..89f07c978 --- /dev/null +++ b/dashboard/src/components/shared/ChangelogDialog.vue @@ -0,0 +1,209 @@ + + + + + + + {{ t('core.navigation.changelogDialog.title') }} + + mdi-close + + + + + + + + + + + {{ t('core.navigation.changelogDialog.current') }} + + + + + + v{{ item.value }} + + + + + + + + + {{ t('core.navigation.changelogDialog.loading') }} + + + {{ changelogError }} + + + + + + + + + + {{ t('core.common.close') }} + + + + + + + diff --git a/dashboard/src/i18n/locales/en-US/core/navigation.json b/dashboard/src/i18n/locales/en-US/core/navigation.json index e1aaf9fd8..52f1eb110 100644 --- a/dashboard/src/i18n/locales/en-US/core/navigation.json +++ b/dashboard/src/i18n/locales/en-US/core/navigation.json @@ -15,10 +15,19 @@ "knowledgeBase": "Knowledge Base", "about": "About", "settings": "Settings", + "changelog": "Changelog", "documentation": "Documentation", "github": "GitHub", "drag": "Drag", "groups": { "more": "More Features" + }, + "changelogDialog": { + "title": "Changelog", + "loading": "Loading...", + "error": "Failed to load", + "notFound": "Changelog for this version not found", + "selectVersion": "Select Version", + "current": "Current" } } diff --git a/dashboard/src/i18n/locales/zh-CN/core/navigation.json b/dashboard/src/i18n/locales/zh-CN/core/navigation.json index b7d2d174c..519de9c25 100644 --- a/dashboard/src/i18n/locales/zh-CN/core/navigation.json +++ b/dashboard/src/i18n/locales/zh-CN/core/navigation.json @@ -15,10 +15,19 @@ "knowledgeBase": "知识库", "about": "关于", "settings": "设置", + "changelog": "更新日志", "documentation": "官方文档", "github": "GitHub", "drag": "拖拽", "groups": { "more": "更多功能" + }, + "changelogDialog": { + "title": "更新日志", + "loading": "加载中...", + "error": "加载失败", + "notFound": "未找到该版本的更新日志", + "selectVersion": "选择版本", + "current": "当前" } } \ No newline at end of file diff --git a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue index 2b2962281..4dc79356a 100644 --- a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue +++ b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue @@ -696,7 +696,7 @@ const changeLanguage = async (langCode: string) => { /* 响应式布局样式 */ .logo-container { - margin-left: 16px; + margin-left: 10px; display: flex; align-items: center; gap: 8px; diff --git a/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue b/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue index f752f6c0f..49ec649ba 100644 --- a/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue +++ b/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue @@ -5,6 +5,7 @@ import { useI18n } from '@/i18n/composables'; import sidebarItems from './sidebarItem'; import NavItem from './NavItem.vue'; import { applySidebarCustomization } from '@/utils/sidebarCustomization'; +import ChangelogDialog from '@/components/shared/ChangelogDialog.vue'; const { t } = useI18n(); @@ -37,6 +38,9 @@ onUnmounted(() => { const showIframe = ref(false); const starCount = ref(null); +// 更新日志对话框 +const changelogDialog = ref(false); + const sidebarWidth = ref(235); const minSidebarWidth = 200; const maxSidebarWidth = 300; @@ -220,6 +224,11 @@ async function fetchStarCount() { fetchStarCount(); +// 打开更新日志对话框 +function openChangelogDialog() { + changelogDialog.value = true; +} + @@ -243,6 +252,9 @@ fetchStarCount(); 🔧 {{ t('core.navigation.settings') }} + + 📝 {{ t('core.navigation.changelog') }} + {{ t('core.navigation.documentation') }} @@ -301,8 +313,11 @@ fetchStarCount(); + > + + +