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') }}
-
+
!
diff --git a/desktop/lib/backend-manager.js b/desktop/lib/backend-manager.js
index 753ee4af4..968ee4b19 100644
--- a/desktop/lib/backend-manager.js
+++ b/desktop/lib/backend-manager.js
@@ -255,6 +255,9 @@ class BackendManager {
...process.env,
PYTHONUNBUFFERED: '1',
};
+ if (this.app.isPackaged) {
+ env.ASTRBOT_ELECTRON_CLIENT = '1';
+ }
if (backendConfig.rootDir) {
env.ASTRBOT_ROOT = backendConfig.rootDir;
const logsDir = path.join(backendConfig.rootDir, 'logs');