From 1addd5b2ab116ed26313367a3dae9464fb1c259b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A8=E3=82=A4=E3=82=AB=E3=82=AF?= <62183434+zouyonghe@users.noreply.github.com> Date: Tue, 10 Feb 2026 00:12:18 +0900 Subject: [PATCH] =?UTF-8?q?perf:=20=E7=A8=B3=E5=AE=9A=E6=BA=90=E7=A0=81?= =?UTF-8?q?=E4=B8=8E=20Electron=20=E6=89=93=E5=8C=85=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E4=B8=8B=E7=9A=84=20pip=20=E5=AE=89=E8=A3=85=E8=A1=8C=E4=B8=BA?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E4=BF=AE=E5=A4=8D=E9=9D=9E=20Electron=20?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=E4=B8=8B=E7=82=B9=E5=87=BB=20WebUI=20?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=8C=89=E9=92=AE=E6=97=B6=E5=87=BA=E7=8E=B0?= =?UTF-8?q?=E8=B7=B3=E8=BD=AC=E5=AF=B9=E8=AF=9D=E6=A1=86=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=20(#4996)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: handle pip install execution in frozen runtime * fix: harden pip subprocess fallback handling * fix: scope global data root to packaged electron runtime * refactor: inline frozen runtime check for electron guard * fix: prefer current interpreter for source pip installs * fix: avoid resolving venv python symlink for pip * refactor: share runtime environment detection utilities * fix: improve error message when pip module is unavailable * fix: raise ImportError when pip module is unavailable * fix: preserve ImportError semantics for missing pip * fix: 修复非electron app环境更新时仍然显示electron更新对话框的问题 --------- Co-authored-by: Soulter <905617992@qq.com> --- astrbot/core/utils/astrbot_path.py | 4 + astrbot/core/utils/pip_installer.py | 102 +++--------------- astrbot/core/utils/runtime_env.py | 10 ++ .../full/vertical-header/VerticalHeader.vue | 16 ++- desktop/lib/backend-manager.js | 3 + 5 files changed, 42 insertions(+), 93 deletions(-) create mode 100644 astrbot/core/utils/runtime_env.py 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') }} -