Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
593a3d8
fix(desktop): make frozen pip installs independent of system python
zouyonghe Feb 17, 2026
86d904c
refactor: migrate desktop backend to cpython runtime
zouyonghe Feb 17, 2026
42a151f
fix(desktop): stabilize CI runtime and windows backend cleanup
zouyonghe Feb 18, 2026
cb15255
fix: write knowledge base under astrbot data dir
zouyonghe Feb 18, 2026
6f6d45b
fix(ci): support branch-based manual release runs
zouyonghe Feb 18, 2026
3c7fd49
fix(desktop): harden runtime packaging and windows cleanup
zouyonghe Feb 18, 2026
07bb9d2
fix(desktop): validate runtime python and harden windows pid checks
zouyonghe Feb 18, 2026
a858939
fix(desktop): harden cleanup fallback and runtime version detection
zouyonghe Feb 18, 2026
fe94a0a
fix(desktop): improve runtime validation and backend cleanup
zouyonghe Feb 18, 2026
c6c238e
fix(desktop): refactor packaged backend runtime resolution
zouyonghe Feb 18, 2026
f63f960
fix(desktop): harden packaged launch fallback and cleanup matching
zouyonghe Feb 18, 2026
d70a75f
refactor(desktop): split backend runtime helpers and cleanup logic
zouyonghe Feb 18, 2026
fb2a41b
refactor(core): remove pyinstaller-specific reboot reset
zouyonghe Feb 18, 2026
4260a95
refactor(core): remove frozen-runtime legacy branches
zouyonghe Feb 18, 2026
9f99b95
docs: use venv runtime env var in desktop readme
zouyonghe Feb 18, 2026
5512afb
fix: block desktop packaged online update paths
zouyonghe Feb 18, 2026
8a1f58a
fix: use writable cwd for packaged backend runtime
zouyonghe Feb 18, 2026
0300d56
refactor: simplify desktop runtime build and cleanup flow
zouyonghe Feb 18, 2026
d5ab04b
fix: normalize nested css selectors and simplify runtime probe parsing
zouyonghe Feb 18, 2026
8761e11
fix: scope css selector normalization and simplify backend launch flow
zouyonghe Feb 18, 2026
8834425
refactor: remove legacy frozen runtime compatibility path
zouyonghe Feb 18, 2026
72791a4
refactor: inline python runtime probe parsing flow
zouyonghe Feb 18, 2026
cdb8b26
refactor: simplify desktop backend build and launch strategy flow
zouyonghe Feb 18, 2026
4ca9618
fix: avoid auto-cleanup on plugin load failure and improve reload checks
zouyonghe Feb 18, 2026
b9c0945
fix: avoid packaging virtualenv as desktop runtime
zouyonghe Feb 18, 2026
184dd4b
refactor: simplify backend launch flow and runtime probe errors
zouyonghe Feb 18, 2026
e1b0a0f
docs: add troubleshooting note for requires-python probe failures
zouyonghe Feb 18, 2026
7365cc2
refactor: streamline backend config and unmanaged cleanup flow
zouyonghe Feb 18, 2026
e9920d1
fix(ci): package relocatable cpython runtime for desktop
zouyonghe Feb 18, 2026
3108863
fix(desktop): install runtime deps into packaged python
zouyonghe Feb 18, 2026
bdc963c
fix(desktop): retry pip install for uv-managed runtime
zouyonghe Feb 18, 2026
5257548
fix(ci): use setup-python runtime source for desktop packaging
zouyonghe Feb 18, 2026
126954f
refactor(ci): remove obsolete uv fallback paths
zouyonghe Feb 18, 2026
3a85efb
refactor(desktop): remove unused electron runtime APIs
zouyonghe Feb 18, 2026
650039a
fix(ci): use python-build-standalone runtime for desktop packaging
zouyonghe Feb 18, 2026
4987911
chore(ci): remove runtime import smoke check
zouyonghe Feb 18, 2026
3d86324
fix(desktop): add windows dll search paths for bundled runtime
zouyonghe Feb 18, 2026
8e15e80
fix(desktop): harden windows dll resolution in launcher
zouyonghe Feb 18, 2026
4e30186
refactor(ci): rebuild windows desktop release jobs
zouyonghe Feb 18, 2026
b46bd76
fix(ci): avoid cryptography source build on windows arm64
zouyonghe Feb 18, 2026
908c367
fix(desktop): bundle msvc runtime for windows backend
zouyonghe Feb 18, 2026
ac27872
fix(desktop): force utf-8 backend log output on windows
zouyonghe Feb 19, 2026
669d6d8
fix: make tray backend restart always run in main process
zouyonghe Feb 19, 2026
38f8d55
fix: enforce wheel-only plugin dependency installs in packaged runtime
zouyonghe Feb 19, 2026
ae9b164
refactor: simplify backend cleanup flow and extract vite postcss plugin
zouyonghe Feb 19, 2026
bca8ab1
refactor(ci): deduplicate packaged cpython runtime resolution
zouyonghe Feb 19, 2026
6f8417b
refactor: simplify windows cleanup state and harden runtime CI checks
zouyonghe Feb 19, 2026
cd16451
fix(ci): pass github token to runtime resolver
zouyonghe Feb 19, 2026
ed53778
fix(desktop): disable blockmap outputs and add jsonschema dependency
zouyonghe Feb 19, 2026
4e749bb
refactor: simplify backend cleanup and centralize pbs mapping
zouyonghe Feb 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ desktop/dist/
desktop/out/
desktop/resources/backend/astrbot-backend*
desktop/resources/backend/*.exe
desktop/resources/backend/app/
desktop/resources/backend/python/
desktop/resources/backend/launch_backend.py
desktop/resources/backend/runtime-manifest.json
desktop/resources/webui/*
desktop/resources/.pyinstaller/
package-lock.json
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ paru -S astrbot-git
#### 桌面端 Electron 打包

桌面端(Electron 打包,`pnpm` 工作流)构建流程请参阅:[`desktop/README.md`](desktop/README.md)。
打包前需要准备 CPython 运行时目录,并设置 `ASTRBOT_DESKTOP_CPYTHON_HOME`(详见该文档)。

## 支持的消息平台

Expand Down
18 changes: 18 additions & 0 deletions astrbot/core/star/star_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@ async def _check_plugin_dept_update(
await pip_installer.install(requirements_path=pth)
except Exception as e:
logger.error(f"更新插件 {p} 的依赖失败。Code: {e!s}")
if target_plugin:
raise Exception(
"插件依赖安装失败,请检查插件 requirements.txt "
"中的依赖版本或构建环境。"
) from e
return True

async def _import_plugin_with_dependency_recovery(
Expand Down Expand Up @@ -471,6 +476,19 @@ async def load(self, specified_module_path=None, specified_dir_name=None):
except Exception as e:
logger.error(traceback.format_exc())
logger.error(f"插件 {root_dir_name} 导入失败。原因:{e!s}")
fail_rec += f"加载 {root_dir_name} 插件时出现问题,原因 {e!s}。\n"
self.failed_plugin_dict[root_dir_name] = {
"error": str(e),
"traceback": traceback.format_exc(),
}
if not reserved:
logger.warning(
f"插件 {root_dir_name} 导入失败,已自动卸载该插件。"
)
await self._cleanup_failed_plugin_install(
dir_name=root_dir_name,
plugin_path=plugin_dir_path,
)
continue

# 检查 _conf_schema.json
Expand Down
127 changes: 18 additions & 109 deletions astrbot/core/utils/pip_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

logger = logging.getLogger("astrbot")

_DISTLIB_FINDER_PATCH_ATTEMPTED = False
_SITE_PACKAGES_IMPORT_LOCK = threading.RLock()


Expand All @@ -34,7 +33,7 @@ def _get_pip_main():
raise ImportError(
"pip module is unavailable "
f"(sys.executable={sys.executable}, "
f"frozen={getattr(sys, 'frozen', False)}, "
f"packaged_electron={is_packaged_electron_runtime()}, "
f"ASTRBOT_ELECTRON_CLIENT={os.environ.get('ASTRBOT_ELECTRON_CLIENT')})"
) from exc

Expand Down Expand Up @@ -426,110 +425,6 @@ def _ensure_plugin_dependencies_preferred(
_ensure_preferred_modules(candidate_modules, target_site_packages)


def _get_loader_for_package(package: object) -> object | None:
loader = getattr(package, "__loader__", None)
if loader is not None:
return loader

spec = getattr(package, "__spec__", None)
if spec is None:
return None
return getattr(spec, "loader", None)


def _try_register_distlib_finder(
distlib_resources: object,
finder_registry: dict[type, object],
register_finder,
resource_finder: object,
loader: object,
package_name: str,
) -> bool:
loader_type = type(loader)
if loader_type in finder_registry:
return False

try:
register_finder(loader, resource_finder)
except Exception as exc:
logger.warning(
"Failed to patch pip distlib finder for loader %s (%s): %s",
loader_type.__name__,
package_name,
exc,
)
return False

updated_registry = getattr(distlib_resources, "_finder_registry", finder_registry)
if isinstance(updated_registry, dict) and loader_type not in updated_registry:
logger.warning(
"Distlib finder patch did not take effect for loader %s (%s).",
loader_type.__name__,
package_name,
)
return False

logger.info(
"Patched pip distlib finder for frozen loader: %s (%s)",
loader_type.__name__,
package_name,
)
return True


def _patch_distlib_finder_for_frozen_runtime() -> None:
global _DISTLIB_FINDER_PATCH_ATTEMPTED

if not getattr(sys, "frozen", False):
return
if _DISTLIB_FINDER_PATCH_ATTEMPTED:
return

_DISTLIB_FINDER_PATCH_ATTEMPTED = True

try:
from pip._vendor.distlib import resources as distlib_resources
except Exception:
return

finder_registry = getattr(distlib_resources, "_finder_registry", None)
register_finder = getattr(distlib_resources, "register_finder", None)
resource_finder = getattr(distlib_resources, "ResourceFinder", None)

if not isinstance(finder_registry, dict):
logger.warning(
"Skip patching distlib finder because _finder_registry is unavailable."
)
return
if not callable(register_finder) or resource_finder is None:
logger.warning(
"Skip patching distlib finder because register API is unavailable."
)
return

for package_name in ("pip._vendor.distlib", "pip._vendor"):
try:
package = importlib.import_module(package_name)
except Exception:
continue

loader = _get_loader_for_package(package)
if loader is None:
continue

if _try_register_distlib_finder(
distlib_resources,
finder_registry,
register_finder,
resource_finder,
loader,
package_name,
):
finder_registry = getattr(
distlib_resources, "_finder_registry", finder_registry
)


class PipInstaller:
def __init__(self, pip_install_arg: str, pypi_index_url: str | None = None) -> None:
self.pip_install_arg = pip_install_arg
Expand All @@ -540,8 +435,10 @@ async def install(
package_name: str | None = None,
requirements_path: str | None = None,
mirror: str | None = None,
wheel_only: bool = False,
) -> None:
args = ["install"]
pip_install_args = self.pip_install_arg.split() if self.pip_install_arg else []
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
requested_requirements: set[str] = set()
if package_name:
args.append(package_name)
Expand All @@ -563,13 +460,26 @@ async def install(
args.extend(["--target", target_site_packages])
args.extend(["--upgrade", "--force-reinstall"])

if self.pip_install_arg:
args.extend(self.pip_install_arg.split())
if pip_install_args:
args.extend(pip_install_args)
if wheel_only:
if not any(
token == "--only-binary" or token.startswith("--only-binary=")
for token in args
):
args.extend(["--only-binary", ":all:"])
if "--prefer-binary" not in args:
args.append("--prefer-binary")

logger.info(f"Pip 包管理器: pip {' '.join(args)}")
result_code = await self._run_pip_in_process(args)
Comment thread
sourcery-ai[bot] marked this conversation as resolved.

if result_code != 0:
if wheel_only:
raise Exception(
"安装失败:插件依赖 wheel-only 检测未通过或依赖安装失败,"
"请检查是否存在无可用 wheel 的依赖。"
)
raise Exception(f"安装失败,错误码:{result_code}")

if target_site_packages:
Expand Down Expand Up @@ -602,7 +512,6 @@ def prefer_installed_dependencies(self, requirements_path: str) -> None:

async def _run_pip_in_process(self, args: list[str]) -> int:
pip_main = _get_pip_main()
_patch_distlib_finder_for_frozen_runtime()

original_handlers = list(logging.getLogger().handlers)
result_code, output = await asyncio.to_thread(
Expand Down
2 changes: 1 addition & 1 deletion astrbot/core/utils/runtime_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ def is_frozen_runtime() -> bool:


def is_packaged_electron_runtime() -> bool:
return is_frozen_runtime() and os.environ.get("ASTRBOT_ELECTRON_CLIENT") == "1"
return os.environ.get("ASTRBOT_ELECTRON_CLIENT") == "1"
39 changes: 36 additions & 3 deletions desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ This document describes how to build the Electron desktop app from source.

- Electron desktop shell (`desktop/main.js`)
- Bundled WebUI static files (`desktop/resources/webui`)
- Bundled backend runtime payload (`desktop/resources/backend`)
- App assets (`desktop/assets`)

Current behavior:

- Backend executable is bundled in the installer/package.
- Backend CPython runtime and source are bundled in the installer/package.
- App startup checks backend availability and auto-starts bundled backend when needed.
- Runtime data is stored under `~/.astrbot` by default, not as a full AstrBot source project.

Expand All @@ -19,6 +20,7 @@ Current behavior:
- Python environment ready in repository root (`uv` available)
- Node.js available
- `pnpm` available
- A CPython runtime directory for packaged backend (contains runnable `python` and `site-packages`)

Desktop dependency management uses `pnpm` with a lockfile:

Expand All @@ -31,12 +33,21 @@ Run commands from repository root:

```bash
uv sync
export ASTRBOT_DESKTOP_CPYTHON_HOME=/path/to/cpython-runtime
pnpm --dir dashboard install
pnpm --dir dashboard build
pnpm --dir desktop install --frozen-lockfile
pnpm --dir desktop run dist:full
```

If you are already developing in this repository, you can directly reuse the local virtual environment as runtime:
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated

```bash
uv sync
export ASTRBOT_DESKTOP_CPYTHON_HOME="$(pwd)/.venv"
pnpm --dir desktop run build:backend
```

Output files are generated under:

- `desktop/dist/`
Expand All @@ -57,9 +68,25 @@ pnpm --dir desktop run dev

## Notes

- `dist:full` runs WebUI build + backend build + Electron packaging.
- `dist:full` runs WebUI build + backend runtime packaging + Electron packaging.
- In packaged app mode, backend data root defaults to `~/.astrbot` (can be overridden by `ASTRBOT_ROOT`).
- Backend build uses `uv run --with pyinstaller ...`, so no manual `PyInstaller` install is required.
- Backend build requires `ASTRBOT_DESKTOP_CPYTHON_HOME` (or `ASTRBOT_DESKTOP_BACKEND_RUNTIME`) to point to a CPython runtime directory.
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated

## Packaged Backend Layout

After `pnpm --dir desktop run build:backend`, backend payload is generated in `desktop/resources/backend`:

```text
desktop/resources/backend/
app/ # AstrBot backend source snapshot used in packaged mode
python/ # Bundled CPython runtime directory
launch_backend.py # Launcher executed by Electron
runtime-manifest.json # Runtime metadata (python path, entrypoint, app path)
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
```

Electron reads `runtime-manifest.json` and starts backend with:
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
- `python` from `python/`
- `launch_backend.py` as entrypoint

## Runtime Directory Layout

Expand Down Expand Up @@ -122,6 +149,12 @@ Backend auto-start:
- `ASTRBOT_BACKEND_AUTO_START=0` disables Electron-managed backend startup.
- When disabled, backend must already be running at `ASTRBOT_BACKEND_URL` before launching app.

Backend build errors:

- `Missing CPython runtime source`: set `ASTRBOT_DESKTOP_CPYTHON_HOME` (or `ASTRBOT_DESKTOP_BACKEND_RUNTIME`).
- `Cannot find Python executable in runtime`: runtime directory is invalid or incomplete.
- `Failed to detect purelib from runtime python`: runtime Python cannot run correctly.
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated

If Electron download times out on restricted networks, configure mirrors before install:

```bash
Expand Down
Loading