Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 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
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
576 changes: 539 additions & 37 deletions .github/workflows/release.yml

Large diffs are not rendered by default.

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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ paru -S astrbot-git
#### 桌面端 Electron 打包

桌面端(Electron 打包,`pnpm` 工作流)构建流程请参阅:[`desktop/README.md`](desktop/README.md)。
打包前需要准备 CPython 运行时目录,并设置 `ASTRBOT_DESKTOP_CPYTHON_HOME`(详见该文档)。
建议使用 `python-build-standalone` 的 `install_only` 运行时作为分发基线。

## 支持的消息平台

Expand Down
7 changes: 5 additions & 2 deletions astrbot/core/knowledge_base/kb_db_sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@
KBMedia,
KnowledgeBase,
)
from astrbot.core.utils.astrbot_path import get_astrbot_knowledge_base_path


class KBSQLiteDatabase:
def __init__(self, db_path: str = "data/knowledge_base/kb.db") -> None:
def __init__(self, db_path: str | None = None) -> None:
"""初始化知识库数据库

Args:
db_path: 数据库文件路径, 默认为 data/knowledge_base/kb.db
db_path: 数据库文件路径, 默认位于 AstrBot 数据目录下的 knowledge_base/kb.db

"""
if db_path is None:
db_path = str(Path(get_astrbot_knowledge_base_path()) / "kb.db")
self.db_path = db_path
self.DATABASE_URL = f"sqlite+aiosqlite:///{db_path}"
self.inited = False
Expand Down
5 changes: 3 additions & 2 deletions astrbot/core/knowledge_base/kb_mgr.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from astrbot.core import logger
from astrbot.core.provider.manager import ProviderManager
from astrbot.core.utils.astrbot_path import get_astrbot_knowledge_base_path

# from .chunking.fixed_size import FixedSizeChunker
from .chunking.recursive import RecursiveCharacterChunker
Expand All @@ -13,7 +14,7 @@
from .retrieval.rank_fusion import RankFusion
from .retrieval.sparse_retriever import SparseRetriever

FILES_PATH = "data/knowledge_base"
FILES_PATH = get_astrbot_knowledge_base_path()
DB_PATH = Path(FILES_PATH) / "kb.db"
"""Knowledge Base storage root directory"""
CHUNKER = RecursiveCharacterChunker()
Expand All @@ -27,7 +28,7 @@ def __init__(
self,
provider_manager: ProviderManager,
) -> None:
Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True)
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
self.provider_manager = provider_manager
self._session_deleted_callback_registered = False

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
import os
import re
from datetime import datetime
from pathlib import Path
from typing import cast

from funasr_onnx import SenseVoiceSmall
from funasr_onnx.utils.postprocess_utils import rich_transcription_postprocess

from astrbot.core import logger
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
from astrbot.core.utils.io import download_file
from astrbot.core.utils.tencent_record_helper import tencent_silk_to_wav

Expand Down Expand Up @@ -50,7 +52,9 @@ async def initialize(self) -> None:

async def get_timestamped_path(self) -> str:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
return os.path.join("data", "temp", f"{timestamp}")
temp_dir = Path(get_astrbot_temp_path())
temp_dir.mkdir(parents=True, exist_ok=True)
return str(temp_dir / timestamp)

async def _is_silk_file(self, file_path) -> bool:
silk_header = b"SILK"
Expand Down
74 changes: 55 additions & 19 deletions astrbot/core/star/star_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@
logger.warning("未安装 watchfiles,无法实现插件的热重载。")


class PluginDependencyInstallError(Exception):
def __init__(self, plugin_name: str, requirements_path: str, message: str) -> None:
super().__init__(message)
self.plugin_name = plugin_name
self.requirements_path = requirements_path


class PluginManager:
def __init__(self, context: Context, config: AstrBotConfig) -> None:
self.updator = PluginUpdator()
Expand Down Expand Up @@ -194,6 +201,13 @@ 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 PluginDependencyInstallError(
plugin_name=p,
requirements_path=pth,
message="插件依赖安装失败,请检查插件 requirements.txt "
"中的依赖版本或构建环境。",
) from e
return True

async def _import_plugin_with_dependency_recovery(
Expand Down Expand Up @@ -343,13 +357,21 @@ async def reload_failed_plugin(self, dir_name):
async with self._pm_lock:
if dir_name in self.failed_plugin_dict:
success, error = await self.load(specified_dir_name=dir_name)
if success:
self.failed_plugin_dict.pop(dir_name, None)
if not self.failed_plugin_dict:
self.failed_plugin_info = ""
return success, None
else:
if not success:
return False, error
plugin_reloaded = any(
star.root_dir_name == dir_name
for star in self.context.get_all_stars()
)
if not plugin_reloaded:
return (
False,
f"插件 {dir_name} 重载失败:未在插件目录中找到可加载插件。",
)
self.failed_plugin_dict.pop(dir_name, None)
if not self.failed_plugin_dict:
self.failed_plugin_info = ""
return True, None
return False, "插件不存在于失败列表中"

async def reload(self, specified_plugin_name=None):
Expand Down Expand Up @@ -431,6 +453,8 @@ async def load(self, specified_module_path=None, specified_dir_name=None):
return False, "未找到任何插件模块"

fail_rec = ""
has_target_filter = bool(specified_module_path or specified_dir_name)
matched_target = False

# 导入插件模块,并尝试实例化插件类
for plugin_module in plugin_modules:
Expand All @@ -457,6 +481,7 @@ async def load(self, specified_module_path=None, specified_dir_name=None):
continue
if specified_dir_name and root_dir_name != specified_dir_name:
continue
matched_target = True

logger.info(f"正在载入插件 {root_dir_name} ...")

Expand All @@ -471,6 +496,15 @@ 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}插件安装失败,插件目录:{plugin_dir_path}"
)
continue

# 检查 _conf_schema.json
Expand Down Expand Up @@ -706,6 +740,10 @@ async def load(self, specified_module_path=None, specified_dir_name=None):
logger.error(f"同步指令配置失败: {e!s}")
logger.error(traceback.format_exc())

if has_target_filter and not matched_target:
target_label = specified_dir_name or specified_module_path
return False, f"未找到指定插件:{target_label}"

if not fail_rec:
return True, None
self.failed_plugin_info = fail_rec
Expand Down Expand Up @@ -781,10 +819,8 @@ async def install_plugin(self, repo_url: str, proxy=""):
async with self._pm_lock:
plugin_path = ""
dir_name = ""
cleanup_required = False
try:
plugin_path = await self.updator.install(repo_url, proxy)
cleanup_required = True

# reload the plugin
dir_name = os.path.basename(plugin_path)
Expand Down Expand Up @@ -829,10 +865,12 @@ async def install_plugin(self, repo_url: str, proxy=""):

return plugin_info
except Exception:
if cleanup_required and dir_name and plugin_path:
await self._cleanup_failed_plugin_install(
dir_name=dir_name,
plugin_path=plugin_path,
if plugin_path:
plugin_dir_name = dir_name or os.path.basename(plugin_path)
logger.warning(
"%s插件安装失败,插件目录:%s",
plugin_dir_name,
plugin_path,
)
raise

Expand Down Expand Up @@ -1096,7 +1134,6 @@ async def install_plugin_from_file(self, zip_file_path: str):
dir_name = os.path.basename(zip_file_path).replace(".zip", "")
dir_name = dir_name.removesuffix("-master").removesuffix("-main").lower()
desti_dir = os.path.join(self.plugin_store_path, dir_name)
cleanup_required = False

# 第一步:检查是否已安装同目录名的插件,先终止旧插件
existing_plugin = None
Expand All @@ -1118,7 +1155,6 @@ async def install_plugin_from_file(self, zip_file_path: str):

try:
self.updator.unzip_file(zip_file_path, desti_dir)
cleanup_required = True

# 第二步:解压后,读取新插件的 metadata.yaml,检查是否存在同名但不同目录的插件
try:
Expand Down Expand Up @@ -1195,9 +1231,9 @@ async def install_plugin_from_file(self, zip_file_path: str):

return plugin_info
except Exception:
if cleanup_required:
await self._cleanup_failed_plugin_install(
dir_name=dir_name,
plugin_path=desti_dir,
)
logger.warning(
"%s插件安装失败,插件目录:%s",
dir_name,
desti_dir,
)
raise
66 changes: 5 additions & 61 deletions astrbot/core/updator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from astrbot.core.config.default import VERSION
from astrbot.core.utils.astrbot_path import get_astrbot_path
from astrbot.core.utils.io import download_file
from astrbot.core.utils.update_guard import ensure_packaged_update_allowed

from .zip_updator import ReleaseInfo, RepoZipUpdator

Expand Down Expand Up @@ -44,71 +45,14 @@ def terminate_child_processes(self) -> None:
except psutil.NoSuchProcess:
pass

@staticmethod
def _is_option_arg(arg: str) -> bool:
return arg.startswith("-")

@classmethod
def _collect_flag_values(cls, argv: list[str], flag: str) -> str | None:
try:
idx = argv.index(flag)
except ValueError:
return None

if idx + 1 >= len(argv):
return None

value_parts: list[str] = []
for arg in argv[idx + 1 :]:
if cls._is_option_arg(arg):
break
if arg:
value_parts.append(arg)

if not value_parts:
return None

return " ".join(value_parts).strip() or None

@classmethod
def _resolve_webui_dir_arg(cls, argv: list[str]) -> str | None:
return cls._collect_flag_values(argv, "--webui-dir")

def _build_frozen_reboot_args(self) -> list[str]:
argv = list(sys.argv[1:])
webui_dir = self._resolve_webui_dir_arg(argv)
if not webui_dir:
webui_dir = os.environ.get("ASTRBOT_WEBUI_DIR")

if webui_dir:
return ["--webui-dir", webui_dir]
return []

@staticmethod
def _reset_pyinstaller_environment() -> None:
if not getattr(sys, "frozen", False):
return
os.environ["PYINSTALLER_RESET_ENVIRONMENT"] = "1"
for key in list(os.environ.keys()):
if key.startswith("_PYI_"):
os.environ.pop(key, None)

def _build_reboot_argv(self, executable: str) -> list[str]:
if os.environ.get("ASTRBOT_CLI") == "1":
args = sys.argv[1:]
return [executable, "-m", "astrbot.cli.__main__", *args]
if getattr(sys, "frozen", False):
args = self._build_frozen_reboot_args()
return [executable, *args]
return [executable, *sys.argv]

@staticmethod
def _exec_reboot(executable: str, argv: list[str]) -> None:
if os.name == "nt" and getattr(sys, "frozen", False):
quoted_executable = f'"{executable}"' if " " in executable else executable
quoted_args = [f'"{arg}"' if " " in arg else arg for arg in argv[1:]]
os.execl(executable, quoted_executable, *quoted_args)
return
os.execv(executable, argv)

def _reboot(self, delay: int = 3) -> None:
Expand All @@ -121,7 +65,6 @@ def _reboot(self, delay: int = 3) -> None:
executable = sys.executable

try:
self._reset_pyinstaller_environment()
reboot_argv = self._build_reboot_argv(executable)
self._exec_reboot(executable, reboot_argv)
except Exception as e:
Expand All @@ -145,11 +88,12 @@ 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:
update_data = await self.fetch_release_info(self.ASTRBOT_RELEASE_API, latest)
file_url = None

if os.environ.get("ASTRBOT_CLI") or os.environ.get("ASTRBOT_LAUNCHER"):
raise Exception("不支持更新此方式启动的AstrBot") # 避免版本管理混乱
ensure_packaged_update_allowed()

update_data = await self.fetch_release_info(self.ASTRBOT_RELEASE_API, latest)
file_url = None

if latest:
latest_version = update_data[0]["tag_name"]
Expand Down
Loading