diff --git a/astrbot/core/utils/astrbot_path.py b/astrbot/core/utils/astrbot_path.py index 5ddf98dd2..063c8ddfc 100644 --- a/astrbot/core/utils/astrbot_path.py +++ b/astrbot/core/utils/astrbot_path.py @@ -15,6 +15,8 @@ Skills 目录路径:固定为数据目录下的 skills 目录 import os +from astrbot.core.utils.runtime_env import is_packaged_electron_runtime + def get_astrbot_path() -> str: """获取Astrbot项目路径""" @@ -27,6 +29,8 @@ def get_astrbot_root() -> str: """获取Astrbot根目录路径""" if path := os.environ.get("ASTRBOT_ROOT"): return os.path.realpath(path) + if is_packaged_electron_runtime(): + return os.path.realpath(os.path.join(os.path.expanduser("~"), ".astrbot")) return os.path.realpath(os.getcwd()) diff --git a/astrbot/core/utils/pip_installer.py b/astrbot/core/utils/pip_installer.py index a9f441099..d9a84f3ed 100644 --- a/astrbot/core/utils/pip_installer.py +++ b/astrbot/core/utils/pip_installer.py @@ -2,67 +2,30 @@ import asyncio import contextlib import importlib import io -import locale import logging import os -import shutil import sys -from pathlib import Path from astrbot.core.utils.astrbot_path import get_astrbot_site_packages_path +from astrbot.core.utils.runtime_env import is_packaged_electron_runtime logger = logging.getLogger("astrbot") -def _robust_decode(line: bytes) -> str: - """解码字节流,兼容不同平台的编码""" - try: - return line.decode("utf-8").strip() - except UnicodeDecodeError: - pass - try: - return line.decode(locale.getpreferredencoding(False)).strip() - except UnicodeDecodeError: - pass - if sys.platform.startswith("win"): - try: - return line.decode("gbk").strip() - except UnicodeDecodeError: - pass - return line.decode("utf-8", errors="replace").strip() - - -def _is_frozen_runtime() -> bool: - return bool(getattr(sys, "frozen", False)) - - -def _get_pip_subprocess_executable() -> str | None: - candidates = [ - getattr(sys, "_base_executable", None), - sys.executable, - shutil.which("python3"), - shutil.which("python"), - ] - - for candidate in candidates: - if not candidate: - continue - - candidate_path = Path(candidate) - with contextlib.suppress(OSError): - candidate_path = candidate_path.resolve() - - if candidate_path.is_file() and os.access(candidate_path, os.X_OK): - return str(candidate_path) - - return None - - def _get_pip_main(): try: from pip._internal.cli.main import main as pip_main except ImportError: - from pip import main as pip_main + try: + from pip import main as pip_main + except ImportError as exc: + raise ImportError( + "pip module is unavailable " + f"(sys.executable={sys.executable}, " + f"frozen={getattr(sys, 'frozen', False)}, " + f"ASTRBOT_ELECTRON_CLIENT={os.environ.get('ASTRBOT_ELECTRON_CLIENT')})" + ) from exc + return pip_main @@ -102,11 +65,10 @@ class PipInstaller: args.extend(["-r", requirements_path]) index_url = mirror or self.pypi_index_url or "https://pypi.org/simple" - args.extend(["--trusted-host", "mirrors.aliyun.com", "-i", index_url]) target_site_packages = None - if _is_frozen_runtime(): + if is_packaged_electron_runtime(): target_site_packages = get_astrbot_site_packages_path() os.makedirs(target_site_packages, exist_ok=True) args.extend(["--target", target_site_packages]) @@ -115,27 +77,7 @@ class PipInstaller: args.extend(self.pip_install_arg.split()) logger.info(f"Pip 包管理器: pip {' '.join(args)}") - result_code = None - - subprocess_executable = _get_pip_subprocess_executable() - if subprocess_executable: - try: - result_code = await self._run_pip_subprocess( - subprocess_executable, args - ) - except OSError as exc: - logger.warning( - "Failed to launch pip subprocess (%r). Falling back to in-process pip: %s", - subprocess_executable, - exc, - ) - else: - logger.debug( - "No suitable Python executable found for pip subprocess; using in-process pip" - ) - - if result_code is None: - result_code = await self._run_pip_in_process(args) + result_code = await self._run_pip_in_process(args) if result_code != 0: raise Exception(f"安装失败,错误码:{result_code}") @@ -144,25 +86,9 @@ class PipInstaller: sys.path.insert(0, target_site_packages) importlib.invalidate_caches() - async def _run_pip_subprocess(self, executable: str, args: list[str]) -> int: - process = await asyncio.create_subprocess_exec( - executable, - "-m", - "pip", - *args, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.STDOUT, - ) - - assert process.stdout is not None - async for line in process.stdout: - logger.info(_robust_decode(line)) - - await process.wait() - return process.returncode - async def _run_pip_in_process(self, args: list[str]) -> int: pip_main = _get_pip_main() + original_handlers = list(logging.getLogger().handlers) result_code, output = await asyncio.to_thread( _run_pip_main_with_output, pip_main, args diff --git a/astrbot/core/utils/runtime_env.py b/astrbot/core/utils/runtime_env.py new file mode 100644 index 000000000..2eb1bc7e4 --- /dev/null +++ b/astrbot/core/utils/runtime_env.py @@ -0,0 +1,10 @@ +import os +import sys + + +def is_frozen_runtime() -> bool: + return bool(getattr(sys, "frozen", False)) + + +def is_packaged_electron_runtime() -> bool: + return is_frozen_runtime() and os.environ.get("ASTRBOT_ELECTRON_CLIENT") == "1" diff --git a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue index 01284aa39..48cefd3cb 100644 --- a/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue +++ b/dashboard/src/layouts/full/vertical-header/VerticalHeader.vue @@ -45,7 +45,9 @@ let version = ref(''); let releases = ref([]); let updatingDashboardLoading = ref(false); let installLoading = ref(false); -const isElectronApp = ref(false); +const isElectronApp = ref( + typeof window !== 'undefined' && !!window.astrbotDesktop?.isElectron +); const redirectConfirmDialog = ref(false); const pendingRedirectUrl = ref(''); const resolvingReleaseTarget = ref(false); @@ -235,7 +237,9 @@ function checkUpdate() { } else { updateStatus.value = res.data.message; } - dashboardHasNewVersion.value = res.data.data.dashboard_has_new_version; + dashboardHasNewVersion.value = isElectronApp.value + ? false + : res.data.data.dashboard_has_new_version; }) .catch((err) => { if (err.response && err.response.status == 401) { @@ -381,7 +385,9 @@ onMounted(async () => { } catch { isElectronApp.value = false; } - isElectronApp.value = true + if (isElectronApp.value) { + dashboardHasNewVersion.value = false; + } }); @@ -426,7 +432,7 @@ onMounted(async () => { {{ t('core.header.version.hasNewVersion') }} - + {{ t('core.header.version.dashboardHasNewVersion') }} @@ -509,7 +515,7 @@ onMounted(async () => { mdi-arrow-up-circle {{ t('core.header.updateDialog.title') }} -