From 37142fd2539761fdb68663ca7a33990751aa869d Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Thu, 14 May 2026 00:40:15 +0800 Subject: [PATCH] feat: enhance update dialog with progress tracking and localization updates - Added advanced settings option in update dialog for better user control. - Implemented detailed progress tracking for update stages including download size and speed. - Updated localization files for English, Russian, and Chinese to include new strings for update progress and advanced settings. - Improved UI for update dialog with better layout and responsiveness. - Enhanced test coverage for update process including progress tracking. --- astrbot/core/updator.py | 15 +- astrbot/core/utils/io.py | 112 ++- astrbot/core/zip_updator.py | 50 +- astrbot/dashboard/routes/update.py | 194 ++++- .../src/i18n/locales/en-US/core/header.json | 16 +- .../src/i18n/locales/ru-RU/core/header.json | 234 +++--- .../src/i18n/locales/zh-CN/core/header.json | 16 +- .../full/vertical-header/VerticalHeader.vue | 676 +++++++++++++++--- tests/test_dashboard.py | 21 +- 9 files changed, 1104 insertions(+), 230 deletions(-) diff --git a/astrbot/core/updator.py b/astrbot/core/updator.py index 73c837b88..fab293418 100644 --- a/astrbot/core/updator.py +++ b/astrbot/core/updator.py @@ -143,7 +143,14 @@ class AstrBotUpdator(RepoZipUpdator): async def get_releases(self) -> list: return await self.fetch_release_info(self.ASTRBOT_RELEASE_API) - async def update(self, reboot=False, latest=True, version=None, proxy="") -> None: + async def update( + self, + reboot=False, + latest=True, + version=None, + proxy="", + progress_callback=None, + ) -> None: update_data = await self.fetch_release_info(self.ASTRBOT_RELEASE_API, latest) file_url = None @@ -175,7 +182,11 @@ class AstrBotUpdator(RepoZipUpdator): file_url = f"{proxy}/{file_url}" try: - await self._download_file(file_url, "temp.zip") + await self._download_file( + file_url, + "temp.zip", + progress_callback=progress_callback, + ) logger.info("下载 AstrBot Core 更新文件完成,正在执行解压...") self.unzip_file("temp.zip", self.MAIN_PATH) except BaseException as e: diff --git a/astrbot/core/utils/io.py b/astrbot/core/utils/io.py index d06f6a19a..8f3f1688d 100644 --- a/astrbot/core/utils/io.py +++ b/astrbot/core/utils/io.py @@ -1,4 +1,5 @@ import base64 +import inspect import logging import os import shutil @@ -147,7 +148,20 @@ async def download_image_by_url( raise e -async def download_file(url: str, path: str, show_progress: bool = False) -> None: +async def _emit_download_progress(progress_callback, payload: dict) -> None: + if not progress_callback: + return + result = progress_callback(payload) + if inspect.isawaitable(result): + await result + + +async def download_file( + url: str, + path: str, + show_progress: bool = False, + progress_callback=None, +) -> None: """从指定 url 下载文件到指定路径 path""" try: ssl_context = ssl.create_default_context( @@ -168,6 +182,16 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non start_time = time.time() if show_progress: print(f"Downloading: {url} | Size: {total_size / 1024:.2f} KB") + await _emit_download_progress( + progress_callback, + { + "url": url, + "downloaded": 0, + "total": total_size, + "percent": 0, + "speed": 0, + }, + ) with open(path, "wb") as f: while True: chunk = await resp.content.read(8192) @@ -175,17 +199,38 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non break f.write(chunk) downloaded_size += len(chunk) + elapsed_time = ( + time.time() - start_time + if time.time() - start_time > 0 + else 1 + ) + speed = downloaded_size / 1024 / elapsed_time # KB/s + percent = downloaded_size / total_size if total_size > 0 else 0 + await _emit_download_progress( + progress_callback, + { + "url": url, + "downloaded": downloaded_size, + "total": total_size, + "percent": percent, + "speed": speed, + }, + ) if show_progress: - elapsed_time = ( - time.time() - start_time - if time.time() - start_time > 0 - else 1 - ) - speed = downloaded_size / 1024 / elapsed_time # KB/s print( - f"\rProgress: {downloaded_size / total_size:.2%} Speed: {speed:.2f} KB/s", + f"\rProgress: {percent:.2%} Speed: {speed:.2f} KB/s", end="", ) + await _emit_download_progress( + progress_callback, + { + "url": url, + "downloaded": downloaded_size, + "total": total_size, + "percent": 1, + "speed": 0, + }, + ) except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError): # 关闭SSL验证(仅在证书验证失败时作为fallback) logger.warning( @@ -208,6 +253,16 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non start_time = time.time() if show_progress: print(f"Size: {total_size / 1024:.2f} KB | URL: {url}") + await _emit_download_progress( + progress_callback, + { + "url": url, + "downloaded": 0, + "total": total_size, + "percent": 0, + "speed": 0, + }, + ) with open(path, "wb") as f: while True: chunk = await resp.content.read(8192) @@ -215,13 +270,38 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non break f.write(chunk) downloaded_size += len(chunk) + elapsed_time = ( + time.time() - start_time + if time.time() - start_time > 0 + else 1 + ) + speed = downloaded_size / 1024 / elapsed_time # KB/s + percent = downloaded_size / total_size if total_size > 0 else 0 + await _emit_download_progress( + progress_callback, + { + "url": url, + "downloaded": downloaded_size, + "total": total_size, + "percent": percent, + "speed": speed, + }, + ) if show_progress: - elapsed_time = time.time() - start_time - speed = downloaded_size / 1024 / elapsed_time # KB/s print( - f"\rProgress: {downloaded_size / total_size:.2%} Speed: {speed:.2f} KB/s", + f"\rProgress: {percent:.2%} Speed: {speed:.2f} KB/s", end="", ) + await _emit_download_progress( + progress_callback, + { + "url": url, + "downloaded": downloaded_size, + "total": total_size, + "percent": 1, + "speed": 0, + }, + ) if show_progress: print() @@ -268,6 +348,7 @@ async def download_dashboard( latest: bool = True, version: str | None = None, proxy: str | None = None, + progress_callback=None, ) -> None: """下载管理面板文件""" if path is None: @@ -286,6 +367,7 @@ async def download_dashboard( dashboard_release_url, str(zip_path), show_progress=True, + progress_callback=progress_callback, ) except BaseException as _: if latest: @@ -312,12 +394,18 @@ async def download_dashboard( dashboard_release_url, str(zip_path), show_progress=True, + progress_callback=progress_callback, ) else: url = f"https://github.com/AstrBotDevs/astrbot-release-harbour/releases/download/release-{version}/dist.zip" logger.info(f"Downloading AstrBot WebUI from {url}") if proxy: url = f"{proxy}/{url}" - await download_file(url, str(zip_path), show_progress=True) + await download_file( + url, + str(zip_path), + show_progress=True, + progress_callback=progress_callback, + ) with zipfile.ZipFile(zip_path, "r") as z: z.extractall(extract_path) diff --git a/astrbot/core/zip_updator.py b/astrbot/core/zip_updator.py index 15096cc81..83c7da8e4 100644 --- a/astrbot/core/zip_updator.py +++ b/astrbot/core/zip_updator.py @@ -1,6 +1,8 @@ +import inspect import os import re import shutil +import time import zipfile from pathlib import Path from typing import NoReturn @@ -53,18 +55,64 @@ class RepoZipUpdator: return body[:max_len] + "...[truncated]" async def _download_file( - self, url: str, path: str, timeout: float = 1800.0 + self, + url: str, + path: str, + timeout: float = 1800.0, + progress_callback=None, ) -> None: target_path = Path(path) ensure_dir(target_path.parent) + async def _emit_progress(payload: dict) -> None: + if not progress_callback: + return + result = progress_callback(payload) + if inspect.isawaitable(result): + await result + try: async with self._create_httpx_client(timeout=timeout) as client: async with client.stream("GET", url) as response: response.raise_for_status() + headers = getattr(response, "headers", {}) + total_size = int(headers.get("content-length", 0)) + downloaded_size = 0 + start_time = time.time() + await _emit_progress( + { + "url": url, + "downloaded": 0, + "total": total_size, + "percent": 0, + "speed": 0, + }, + ) with target_path.open("wb") as file: async for chunk in response.aiter_bytes(8192): file.write(chunk) + downloaded_size += len(chunk) + elapsed_time = max(time.time() - start_time, 1) + await _emit_progress( + { + "url": url, + "downloaded": downloaded_size, + "total": total_size, + "percent": downloaded_size / total_size + if total_size > 0 + else 0, + "speed": downloaded_size / 1024 / elapsed_time, + }, + ) + await _emit_progress( + { + "url": url, + "downloaded": downloaded_size, + "total": total_size, + "percent": 1, + "speed": 0, + }, + ) except Exception as e: logger.error(f"下载文件失败: {url} -> {target_path}, 错误: {e}") if self.rm_on_error and target_path.exists(): diff --git a/astrbot/dashboard/routes/update.py b/astrbot/dashboard/routes/update.py index b0520c315..210eb2100 100644 --- a/astrbot/dashboard/routes/update.py +++ b/astrbot/dashboard/routes/update.py @@ -1,4 +1,5 @@ import traceback +import uuid from quart import request @@ -24,6 +25,7 @@ class UpdateRoute(Route): super().__init__(context) self.routes = { "/update/check": ("GET", self.check_update), + "/update/progress": ("GET", self.get_update_progress), "/update/releases": ("GET", self.get_releases), "/update/do": ("POST", self.update_project), "/update/dashboard": ("POST", self.update_dashboard), @@ -32,8 +34,104 @@ class UpdateRoute(Route): } self.astrbot_updator = astrbot_updator self.core_lifecycle = core_lifecycle + self.update_progress: dict[str, dict] = {} self.register_routes() + def _init_update_progress(self, progress_id: str, version: str) -> None: + self.update_progress[progress_id] = { + "id": progress_id, + "status": "running", + "stage": "preparing", + "version": version or "latest", + "message": "正在准备更新...", + "overall_percent": 0, + "stages": { + "dashboard": self._empty_stage("pending"), + "core": self._empty_stage("pending"), + }, + } + + @staticmethod + def _empty_stage(status: str = "pending") -> dict: + return { + "status": status, + "downloaded": 0, + "total": 0, + "percent": 0, + "speed": 0, + } + + def _set_update_stage( + self, + progress_id: str, + stage: str, + status: str, + message: str, + overall_percent: int | None = None, + ) -> None: + progress = self.update_progress.get(progress_id) + if not progress: + return + progress["stage"] = stage + progress["message"] = message + progress["stages"].setdefault(stage, self._empty_stage()) + progress["stages"][stage]["status"] = status + if overall_percent is not None: + progress["overall_percent"] = overall_percent + + @staticmethod + def _normalize_percent(value) -> int: + try: + percent = float(value or 0) + except (TypeError, ValueError): + return 0 + if percent <= 1: + percent *= 100 + return max(0, min(100, int(percent))) + + def _make_progress_callback( + self, + progress_id: str, + stage: str, + stage_start: int, + stage_weight: int, + ): + def _callback(payload: dict) -> None: + progress = self.update_progress.get(progress_id) + if not progress: + return + stage_percent = self._normalize_percent(payload.get("percent")) + progress["stage"] = stage + progress["stages"][stage] = { + "status": "running" if stage_percent < 100 else "done", + "downloaded": payload.get("downloaded", 0), + "total": payload.get("total", 0), + "percent": stage_percent, + "speed": payload.get("speed", 0), + } + progress["overall_percent"] = min( + 99, + stage_start + int(stage_percent * stage_weight / 100), + ) + + return _callback + + async def get_update_progress(self): + progress_id = request.args.get("id", "") + if not progress_id: + return Response().error("缺少参数 id。").__dict__ + progress = self.update_progress.get(progress_id) + if not progress: + return ( + Response() + .ok( + {"id": progress_id, "status": "idle"}, + "没有正在进行的更新。", + ) + .__dict__ + ) + return Response().ok(progress).__dict__ + async def do_migration(self): need_migration = await check_migration_needed_v4(self.core_lifecycle.db) if not need_migration: @@ -89,6 +187,7 @@ class UpdateRoute(Route): data = await request.json version = data.get("version", "") reboot = data.get("reboot", True) + progress_id = data.get("progress_id") or uuid.uuid4().hex if version == "" or version == "latest": latest = True version = "" @@ -99,33 +198,112 @@ class UpdateRoute(Route): if proxy: proxy = proxy.removesuffix("/") + self._init_update_progress(progress_id, version) try: + self._set_update_stage( + progress_id, + "dashboard", + "running", + "正在下载 WebUI...", + 0, + ) + await download_dashboard( + latest=latest, + version=version, + proxy=proxy, + progress_callback=self._make_progress_callback( + progress_id, + "dashboard", + 0, + 45, + ), + ) + self._set_update_stage( + progress_id, + "dashboard", + "done", + "WebUI 下载完成。", + 45, + ) + + self._set_update_stage( + progress_id, + "core", + "running", + "正在下载 AstrBot 项目代码...", + 45, + ) await self.astrbot_updator.update( latest=latest, version=version, proxy=proxy, + progress_callback=self._make_progress_callback( + progress_id, + "core", + 45, + 45, + ), + ) + self._set_update_stage( + progress_id, + "core", + "done", + "项目代码下载完成。", + 90, ) - try: - await download_dashboard(latest=latest, version=version, proxy=proxy) - except Exception as e: - logger.error(f"下载管理面板文件失败: {e}。") - # pip 更新依赖 + self._set_update_stage( + progress_id, + "dependencies", + "running", + "正在更新依赖...", + 92, + ) logger.info("更新依赖中...") try: await pip_installer.install(requirements_path="requirements.txt") except Exception as e: logger.error(f"更新依赖失败: {e}") + self._set_update_stage( + progress_id, + "dependencies", + "done", + "依赖更新完成。", + 96, + ) if reboot: + self._set_update_stage( + progress_id, + "restart", + "running", + "更新成功,正在准备重启...", + 98, + ) await self.core_lifecycle.restart() + self.update_progress[progress_id].update( + { + "status": "success", + "stage": "done", + "message": "更新成功,AstrBot 将在 2 秒内全量重启以应用新的代码。", + "overall_percent": 100, + }, + ) ret = ( Response() .ok(None, "更新成功,AstrBot 将在 2 秒内全量重启以应用新的代码。") .__dict__ ) return ret, 200, CLEAR_SITE_DATA_HEADERS + self.update_progress[progress_id].update( + { + "status": "success", + "stage": "done", + "message": "更新成功,AstrBot 将在下次启动时应用新的代码。", + "overall_percent": 100, + }, + ) ret = ( Response() .ok(None, "更新成功,AstrBot 将在下次启动时应用新的代码。") @@ -133,6 +311,12 @@ class UpdateRoute(Route): ) return ret, 200, CLEAR_SITE_DATA_HEADERS except Exception as e: + self.update_progress[progress_id].update( + { + "status": "error", + "message": e.__str__(), + }, + ) logger.error(f"/api/update_project: {traceback.format_exc()}") return Response().error(e.__str__()).__dict__ diff --git a/dashboard/src/i18n/locales/en-US/core/header.json b/dashboard/src/i18n/locales/en-US/core/header.json index 07e913e4c..d83f06152 100644 --- a/dashboard/src/i18n/locales/en-US/core/header.json +++ b/dashboard/src/i18n/locales/en-US/core/header.json @@ -23,6 +23,7 @@ "tabs": { "release": "😊 Release" }, + "advancedSettings": "Advanced settings", "updateToLatest": "Update to Latest Version", "preRelease": "Pre-release", "preReleaseWarning": { @@ -45,7 +46,19 @@ "switch": "Switch" }, "releaseNotes": { - "title": "Release Notes" + "title": "Release Notes", + "latestLabel": "Latest version" + }, + "progress": { + "target": "Target version: ", + "preparing": "Preparing update...", + "dashboard": "Download WebUI", + "core": "Download project code", + "dependencies": "Updating dependencies...", + "restart": "Preparing restart...", + "restarting": "Restarting AstrBot", + "completed": "Update complete", + "failed": "Update failed" }, "redirectConfirm": { "title": "Leaving AstrBot", @@ -75,6 +88,7 @@ "currentVersion": "Current Version", "hasNewVersion": "New version available!", "isLatest": "Already the latest version.", + "fallback": "Use this to re-download the WebUI if the full update path fails.", "downloadAndUpdate": "Download and Update" } }, diff --git a/dashboard/src/i18n/locales/ru-RU/core/header.json b/dashboard/src/i18n/locales/ru-RU/core/header.json index eb2ce7f23..3124cd971 100644 --- a/dashboard/src/i18n/locales/ru-RU/core/header.json +++ b/dashboard/src/i18n/locales/ru-RU/core/header.json @@ -1,113 +1,127 @@ { - "logoTitle": "Панель управления AstrBot", - "version": { - "hasNewVersion": "Доступна новая версия AstrBot!", - "dashboardHasNewVersion": "Доступна новая версия WebUI!" - }, - "buttons": { - "update": "Обновить", - "account": "Аккаунт", - "theme": { - "light": "Светлая тема", - "dark": "Темная тема" - } - }, - "updateDialog": { - "title": "Обновить AstrBot", - "currentVersion": "Текущая версия", - "status": { - "checking": "Проверка обновлений...", - "switching": "Переключение версии...", - "updating": "Обновление..." - }, - "tabs": { - "release": "😊 Релиз" - }, - "updateToLatest": "Обновить до последней версии", - "preRelease": "Предварительная версия", - "preReleaseWarning": { - "title": "Внимание: предварительная версия", - "description": "Версии с меткой Pre-release могут содержать неизвестные ошибки. Не рекомендуется использовать в рабочих средах. Если вы обнаружили ошибку, пожалуйста, сообщите о ней в ", - "issueLink": "GitHub Issues" - }, - "tip": "💡 ПОДСКАЗКА: ", - "tipContinue": "По умолчанию при переключении версии загружаются соответствующие файлы WebUI. Код WebUI находится в директории dashboard, вы можете собрать его самостоятельно с помощью npm.", - "dockerTip": "При переключении версии будет предпринята попытка обновить как основной процесс бота, так и панель управления. Если вы используете Docker, вы также можете обновить образ или использовать", - "dockerTipLink": "watchtower", - "dockerTipContinue": "для автоматического мониторинга и обновления.", - "table": { - "tag": "Тег", - "publishDate": "Дата публикации", - "content": "Содержание", - "sourceUrl": "Исходный код", - "actions": "Действия", - "view": "Просмотр", - "switch": "Переключить" - }, - "releaseNotes": { - "title": "Журнал изменений" - }, - "redirectConfirm": { - "title": "Переход по ссылке", - "message": "Вы будете перенаправлены на страницу GitHub Releases. Продолжить?", - "latestLabel": "Последняя версия", - "targetVersion": "Целевая версия:", - "currentVersion": "Текущая версия:", - "guideTitle": "Рекомендации после перехода:", - "guideStep1": "Загрузите пакет, соответствующий архитектуре вашей системы.", - "guideStep2": "После завершения установки перезапустите AstrBot.", - "guideStep3": "Если вы используете Docker, отдайте приоритет обновлению через образ." - }, - "desktopApp": { - "title": "Обновить десктопное приложение", - "message": "Проверка и обновление десктопной версии AstrBot.", - "currentVersion": "Текущая версия:", - "latestVersion": "Последняя версия:", - "checking": "Проверка обновлений десктопного приложения...", - "hasNewVersion": "Найдена новая версия. Нажмите для подтверждения обновления.", - "isLatest": "Установлена последняя версия", - "installing": "Загрузка и установка обновления... Приложение будет перезапущено автоматически.", - "checkFailed": "Ошибка проверки обновлений. Попробуйте позже.", - "installFailed": "Ошибка обновления. Попробуйте позже." - }, - "dashboardUpdate": { - "title": "Обновить только панель управления", - "currentVersion": "Текущая версия", - "hasNewVersion": "Доступна новая версия!", - "isLatest": "Установлена последняя версия.", - "downloadAndUpdate": "Скачать и обновить" - } - }, - "accountDialog": { - "title": "Изменить аккаунт", - "securityWarning": "Безопасность: Пожалуйста, смените пароль для защиты аккаунта", - "securityWarningLegacy": "Новая версия AstrBot улучшила безопасность. Пожалуйста, измените пароль.", - "securityWarningUpgrade": "Обнаружено устаревшее хранение пароля. Измените пароль, чтобы завершить обновление безопасности.", - "form": { - "currentPassword": "Текущий пароль", - "newPassword": "Новый пароль", - "confirmPassword": "Подтвердите новый пароль", - "newUsername": "Новое имя пользователя (опционально)", - "passwordHint": "Не менее 8 символов, включая заглавные и строчные буквы, а также цифры", - "confirmPasswordHint": "Введите новый пароль еще раз", - "usernameHint": "Оставьте пустым, если не хотите менять имя пользователя", - "defaultCredentials": "Имя пользователя по умолчанию — astrbot. Начальный пароль генерируется при первом запуске и отображается в консоли." - }, - "validation": { - "passwordRequired": "Введите пароль", - "passwordMinLength": "Пароль должен быть не менее 8 символов", - "passwordUppercase": "Пароль должен содержать хотя бы одну заглавную букву", - "passwordLowercase": "Пароль должен содержать хотя бы одну строчную букву", - "passwordDigit": "Пароль должен содержать хотя бы одну цифру", - "passwordMatch": "Паролы не совпадают", - "usernameMinLength": "Имя пользователя должно быть не менее 3 символов" - }, - "actions": { - "save": "Сохранить изменения", - "cancel": "Отмена" - }, - "messages": { - "updateFailed": "Ошибка обновления, попробуйте еще раз" - } + "logoTitle": "Панель управления AstrBot", + "version": { + "hasNewVersion": "Доступна новая версия AstrBot!", + "dashboardHasNewVersion": "Доступна новая версия WebUI!" + }, + "buttons": { + "update": "Обновить", + "account": "Аккаунт", + "theme": { + "light": "Светлая тема", + "dark": "Темная тема" } + }, + "updateDialog": { + "title": "Обновить AstrBot", + "currentVersion": "Текущая версия", + "status": { + "checking": "Проверка обновлений...", + "switching": "Переключение версии...", + "updating": "Обновление..." + }, + "tabs": { + "release": "😊 Релиз" + }, + "advancedSettings": "Расширенные настройки", + "updateToLatest": "Обновить до последней версии", + "preRelease": "Предварительная версия", + "preReleaseWarning": { + "title": "Внимание: предварительная версия", + "description": "Версии с меткой Pre-release могут содержать неизвестные ошибки. Не рекомендуется использовать в рабочих средах. Если вы обнаружили ошибку, пожалуйста, сообщите о ней в ", + "issueLink": "GitHub Issues" + }, + "tip": "💡 ПОДСКАЗКА: ", + "tipContinue": "По умолчанию при переключении версии загружаются соответствующие файлы WebUI. Код WebUI находится в директории dashboard, вы можете собрать его самостоятельно с помощью npm.", + "dockerTip": "При переключении версии будет предпринята попытка обновить как основной процесс бота, так и панель управления. Если вы используете Docker, вы также можете обновить образ или использовать", + "dockerTipLink": "watchtower", + "dockerTipContinue": "для автоматического мониторинга и обновления.", + "table": { + "tag": "Тег", + "publishDate": "Дата публикации", + "content": "Содержание", + "sourceUrl": "Исходный код", + "actions": "Действия", + "view": "Просмотр", + "switch": "Переключить" + }, + "releaseNotes": { + "title": "Журнал изменений", + "latestLabel": "Последняя версия" + }, + "progress": { + "target": "Целевая версия:", + "preparing": "Подготовка обновления...", + "dashboard": "Загрузка WebUI", + "core": "Загрузка кода проекта", + "dependencies": "Обновление зависимостей...", + "restart": "Подготовка перезапуска...", + "restarting": "Перезапуск AstrBot", + "completed": "Обновление завершено", + "failed": "Ошибка обновления" + }, + "redirectConfirm": { + "title": "Переход по ссылке", + "message": "Вы будете перенаправлены на страницу GitHub Releases. Продолжить?", + "latestLabel": "Последняя версия", + "targetVersion": "Целевая версия:", + "currentVersion": "Текущая версия:", + "guideTitle": "Рекомендации после перехода:", + "guideStep1": "Загрузите пакет, соответствующий архитектуре вашей системы.", + "guideStep2": "После завершения установки перезапустите AstrBot.", + "guideStep3": "Если вы используете Docker, отдайте приоритет обновлению через образ." + }, + "desktopApp": { + "title": "Обновить десктопное приложение", + "message": "Проверка и обновление десктопной версии AstrBot.", + "currentVersion": "Текущая версия:", + "latestVersion": "Последняя версия:", + "checking": "Проверка обновлений десктопного приложения...", + "hasNewVersion": "Найдена новая версия. Нажмите для подтверждения обновления.", + "isLatest": "Установлена последняя версия", + "installing": "Загрузка и установка обновления... Приложение будет перезапущено автоматически.", + "checkFailed": "Ошибка проверки обновлений. Попробуйте позже.", + "installFailed": "Ошибка обновления. Попробуйте позже." + }, + "dashboardUpdate": { + "title": "Обновить только панель управления", + "currentVersion": "Текущая версия", + "hasNewVersion": "Доступна новая версия!", + "isLatest": "Установлена последняя версия.", + "fallback": "Используйте это для повторной загрузки WebUI, если полное обновление завершилось ошибкой.", + "downloadAndUpdate": "Скачать и обновить" + } + }, + "accountDialog": { + "title": "Изменить аккаунт", + "securityWarning": "Безопасность: Пожалуйста, смените пароль для защиты аккаунта", + "securityWarningLegacy": "Новая версия AstrBot улучшила безопасность. Пожалуйста, измените пароль.", + "securityWarningUpgrade": "Обнаружено устаревшее хранение пароля. Измените пароль, чтобы завершить обновление безопасности.", + "form": { + "currentPassword": "Текущий пароль", + "newPassword": "Новый пароль", + "confirmPassword": "Подтвердите новый пароль", + "newUsername": "Новое имя пользователя (опционально)", + "passwordHint": "Не менее 8 символов, включая заглавные и строчные буквы, а также цифры", + "confirmPasswordHint": "Введите новый пароль еще раз", + "usernameHint": "Оставьте пустым, если не хотите менять имя пользователя", + "defaultCredentials": "Имя пользователя по умолчанию — astrbot. Начальный пароль генерируется при первом запуске и отображается в консоли." + }, + "validation": { + "passwordRequired": "Введите пароль", + "passwordMinLength": "Пароль должен быть не менее 8 символов", + "passwordUppercase": "Пароль должен содержать хотя бы одну заглавную букву", + "passwordLowercase": "Пароль должен содержать хотя бы одну строчную букву", + "passwordDigit": "Пароль должен содержать хотя бы одну цифру", + "passwordMatch": "Паролы не совпадают", + "usernameMinLength": "Имя пользователя должно быть не менее 3 символов" + }, + "actions": { + "save": "Сохранить изменения", + "cancel": "Отмена" + }, + "messages": { + "updateFailed": "Ошибка обновления, попробуйте еще раз" + } + } } diff --git a/dashboard/src/i18n/locales/zh-CN/core/header.json b/dashboard/src/i18n/locales/zh-CN/core/header.json index 9be4af2a5..8897ad0af 100644 --- a/dashboard/src/i18n/locales/zh-CN/core/header.json +++ b/dashboard/src/i18n/locales/zh-CN/core/header.json @@ -23,6 +23,7 @@ "tabs": { "release": "😊 正式版" }, + "advancedSettings": "高级设置", "updateToLatest": "更新到最新版本", "preRelease": "预发布", "preReleaseWarning": { @@ -45,7 +46,19 @@ "switch": "切换" }, "releaseNotes": { - "title": "更新日志" + "title": "更新日志", + "latestLabel": "最新版本" + }, + "progress": { + "target": "目标版本:", + "preparing": "正在准备更新...", + "dashboard": "下载 WebUI", + "core": "下载项目代码", + "dependencies": "正在更新依赖...", + "restart": "正在准备重启...", + "restarting": "正在重启 AstrBot", + "completed": "更新完成", + "failed": "更新失败" }, "redirectConfirm": { "title": "即将跳转", @@ -75,6 +88,7 @@ "currentVersion": "当前版本", "hasNewVersion": "有新版本!", "isLatest": "已经是最新版本了。", + "fallback": "完整更新异常时,可仅重新下载 WebUI 作为兜底。", "downloadAndUpdate": "下载并更新" } }, diff --git a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue index a1fb67eb8..c41b566e7 100644 --- a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue +++ b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue @@ -1,5 +1,5 @@