Compare commits

...

3 Commits

Author SHA1 Message Date
Soulter
319b2570be fix: keep system tools with persona tool lists 2026-06-20 00:57:43 +08:00
Soulter
a7533aacda fix: fall back to stale WebUI when repair fails 2026-06-20 00:01:29 +08:00
Weilong Liao
46a846b88b chore: bump version to 4.26.0-beta.10 (#8905) 2026-06-19 23:59:39 +08:00
8 changed files with 161 additions and 26 deletions

View File

@@ -457,10 +457,10 @@ async def _ensure_persona_and_skills(
cfg: dict,
plugin_context: Context,
event: AstrMessageEvent,
) -> set[str] | None:
) -> None:
"""Ensure persona and skills are applied to the request's system prompt or user prompt."""
if not req.conversation:
return None
return
(
persona_id,
@@ -527,13 +527,11 @@ async def _ensure_persona_and_skills(
# inject toolset in the persona
if (persona and persona.get("tools") is None) or not persona:
persona_allowed_tools = None
persona_toolset = tmgr.get_full_tool_set()
for tool in list(persona_toolset):
if not tool.active:
persona_toolset.remove_tool(tool.name)
else:
persona_allowed_tools = {str(tool_name) for tool_name in persona["tools"]}
persona_toolset = ToolSet()
if persona["tools"]:
for tool_name in persona["tools"]:
@@ -614,7 +612,6 @@ async def _ensure_persona_and_skills(
)
except Exception:
pass
return persona_allowed_tools
async def _request_img_caption(
@@ -947,13 +944,12 @@ async def _decorate_llm_request(
plugin_context: Context,
config: MainAgentBuildConfig,
provider: Provider | None = None,
) -> set[str] | None:
) -> None:
cfg = config.provider_settings or plugin_context.get_config(
umo=event.unified_msg_origin
).get("provider_settings", {})
_apply_prompt_prefix(req, cfg)
persona_allowed_tools = None
main_provider_supports_image = provider is not None and _provider_supports_modality(
provider, "image"
@@ -962,9 +958,7 @@ async def _decorate_llm_request(
quote_images_already_captioned = False
if req.conversation:
persona_allowed_tools = await _ensure_persona_and_skills(
req, cfg, plugin_context, event
)
await _ensure_persona_and_skills(req, cfg, plugin_context, event)
if img_cap_prov_id and req.image_urls and not main_provider_supports_image:
await _ensure_img_caption(
@@ -993,7 +987,6 @@ async def _decorate_llm_request(
tz = plugin_context.get_config().get("timezone")
_append_system_reminders(event, req, cfg, tz)
_apply_workspace_extra_prompt(event, req)
return persona_allowed_tools
def _plugin_tool_fix(event: AstrMessageEvent, req: ProviderRequest) -> None:
@@ -1522,9 +1515,7 @@ async def build_main_agent(
else:
return None
persona_allowed_tools = await _decorate_llm_request(
event, req, plugin_context, config, provider=provider
)
await _decorate_llm_request(event, req, plugin_context, config, provider=provider)
await _apply_kb(event, req, plugin_context, config)
@@ -1560,11 +1551,6 @@ async def build_main_agent(
)
)
if persona_allowed_tools is not None and req.func_tool:
req.func_tool.tools = [
tool for tool in req.func_tool.tools if tool.name in persona_allowed_tools
]
fallback_providers = _get_fallback_chat_providers(
provider, plugin_context, config.provider_settings
)

View File

@@ -203,10 +203,19 @@ class AstrBotDashboard:
) or is_dashboard_dist_compatible(bundled_dist, VERSION):
self.data_path = str(bundled_dist)
logger.info("Using bundled dashboard dist: %s", self.data_path)
elif (
os.path.exists(user_dist) and (Path(user_dist) / "index.html").is_file()
):
logger.warning(
"Using existing data/dist as a fallback even though WebUI version mismatches core: %s, expected v%s. "
"Some dashboard features may not work until the matching WebUI is available.",
user_version,
VERSION,
)
self.data_path = os.path.abspath(user_dist)
elif os.path.exists(user_dist):
logger.warning(
"Ignoring data/dist because WebUI version mismatches core: %s, expected v%s.",
user_version,
"Ignoring data/dist because WebUI files are incomplete for core v%s.",
VERSION,
)
self.data_path = None

View File

@@ -0,0 +1,22 @@
- [更新日志(简体中文)](#chinese)
- [Changelog(English)](#english)
<a id="chinese"></a>
## What's Changed
### 修复
- 恢复 WebUI 在接口返回 401 时跳转登录页,避免会话失效后停留在异常状态。([#8903](https://github.com/AstrBotDevs/AstrBot/pull/8903))
- 保持 Core 版本与 WebUI 静态资源版本同步,修复打包或升级后可能加载旧 dist、资源版本错配的问题。([#8901](https://github.com/AstrBotDevs/AstrBot/pull/8901))
- 将知识库上下文作为临时 user 内容注入,修复模型请求中知识库上下文角色不准确的问题。([#8904](https://github.com/AstrBotDevs/AstrBot/pull/8904))
<a id="english"></a>
## What's Changed (EN)
### Bug Fixes
- Restored the WebUI login redirect when API requests return 401, preventing expired sessions from staying in a broken state. ([#8903](https://github.com/AstrBotDevs/AstrBot/pull/8903))
- Kept Core and WebUI static asset versions in sync, fixing stale dist loading and asset version mismatches after packaging or upgrades. ([#8901](https://github.com/AstrBotDevs/AstrBot/pull/8901))
- Injected knowledge base context as temporary user content, fixing the role used for knowledge context in model requests. ([#8904](https://github.com/AstrBotDevs/AstrBot/pull/8904))

View File

@@ -160,6 +160,14 @@ async def check_dashboard_files(webui_dir: str | None = None):
)
except Exception as e:
logger.critical(f"下载管理面板文件失败: {e}")
if (data_dist_path / "index.html").is_file():
logger.warning(
"Falling back to existing data/dist WebUI %s even though core expects v%s. "
"Some dashboard features may not work until the matching WebUI is available.",
v or "unknown",
VERSION,
)
return str(data_dist_path)
return None
logger.info("管理面板下载完成。")
return str(data_dist_path)

View File

@@ -1,6 +1,6 @@
[project]
name = "AstrBot"
version = "4.26.0-beta.9"
version = "4.26.0-beta.10"
description = "Easy-to-use multi-platform LLM chatbot and development framework"
readme = "README.md"
license = { text = "AGPL-3.0-or-later" }

View File

@@ -294,7 +294,34 @@ def test_dashboard_uses_bundled_dist_when_data_dist_is_stale(
assert server.data_path == str(bundled_dist)
def test_dashboard_ignores_mismatched_data_dist_without_bundled(
def test_dashboard_falls_back_to_mismatched_data_dist_without_bundled(
core_lifecycle_td: AstrBotCoreLifecycle,
monkeypatch,
tmp_path,
):
data_dir = tmp_path / "data"
user_dist = data_dir / "dist"
bundled_dist = tmp_path / "bundled-dist"
(user_dist / "assets").mkdir(parents=True)
(user_dist / "assets" / "version").write_text("v0.0.1", encoding="utf-8")
(user_dist / "index.html").write_text("stale", encoding="utf-8")
monkeypatch.setattr(
"astrbot.dashboard.server.get_astrbot_data_path",
lambda: str(data_dir),
)
monkeypatch.setattr(
"astrbot.dashboard.server.get_bundled_dashboard_dist_path",
lambda: bundled_dist,
)
shutdown_event = asyncio.Event()
server = AstrBotDashboard(core_lifecycle_td, core_lifecycle_td.db, shutdown_event)
assert server.data_path == str(user_dist)
def test_dashboard_ignores_incomplete_mismatched_data_dist_without_bundled(
core_lifecycle_td: AstrBotCoreLifecycle,
monkeypatch,
tmp_path,

View File

@@ -246,6 +246,44 @@ async def test_check_dashboard_files_exists_but_version_mismatch_downloads(tmp_p
assert "WebUI version mismatch" in call_args[0]
@pytest.mark.asyncio
async def test_check_dashboard_files_falls_back_to_stale_dist_when_download_fails(
tmp_path,
):
"""Tests stale dashboard fallback when the matching WebUI cannot be downloaded."""
from main import VERSION
data_dir = tmp_path / "data"
data_dist = data_dir / "dist"
bundled_dist = tmp_path / "bundled-dist"
(data_dist / "assets").mkdir(parents=True)
(data_dist / "assets" / "version").write_text("v0.0.1", encoding="utf-8")
(data_dist / "index.html").write_text("stale", encoding="utf-8")
with mock.patch("main.get_astrbot_data_path", return_value=str(data_dir)):
with mock.patch(
"main.get_bundled_dashboard_dist_path",
return_value=bundled_dist,
):
with mock.patch(
"main.download_dashboard",
side_effect=RuntimeError("missing dashboard asset"),
) as mock_download:
with mock.patch("main.logger.warning") as mock_logger_warning:
result = await check_dashboard_files()
assert result == str(data_dist)
mock_download.assert_called_once_with(
version=f"v{VERSION}",
latest=False,
allow_insecure_ssl_fallback=False,
)
assert any(
"Falling back to existing data/dist WebUI" in call.args[0]
for call in mock_logger_warning.call_args_list
)
@pytest.mark.asyncio
async def test_check_dashboard_files_downloads_when_matching_dist_is_incomplete(
tmp_path,

View File

@@ -1009,7 +1009,7 @@ class TestEnsurePersonaAndSkills:
assert req.func_tool is not None
@pytest.mark.asyncio
async def test_persona_empty_tools_filters_late_builtin_tools(
async def test_persona_empty_tools_keeps_late_builtin_tools(
self, mock_event, mock_context, mock_provider
):
module = ama
@@ -1017,6 +1017,7 @@ class TestEnsurePersonaAndSkills:
mock_context.persona_manager.resolve_selected_persona = AsyncMock(
return_value=("locked", persona, None, False)
)
mock_event.platform_meta.support_proactive_message = False
mock_context.get_config.return_value = {
"provider_settings": {
"web_search": True,
@@ -1030,6 +1031,7 @@ class TestEnsurePersonaAndSkills:
"websearch_provider": "baidu_ai_search",
},
computer_use_runtime="none",
add_cron_tools=False,
)
req = ProviderRequest(prompt="hello")
req.conversation = MagicMock(persona_id="locked", history="[]")
@@ -1052,9 +1054,52 @@ class TestEnsurePersonaAndSkills:
)
assert result is not None
try:
assert result.provider_request.func_tool is None or (
result.provider_request.func_tool.empty()
assert result.provider_request.func_tool is not None
assert result.provider_request.func_tool.names() == ["web_search_baidu"]
finally:
if result.reset_coro:
result.reset_coro.close()
@pytest.mark.asyncio
async def test_persona_empty_tools_keeps_local_runtime_builtin_tools(
self, mock_event, mock_context, mock_provider
):
module = ama
persona = {"name": "locked", "prompt": "No tools.", "tools": []}
mock_context.persona_manager.resolve_selected_persona = AsyncMock(
return_value=("locked", persona, None, False)
)
mock_event.platform_meta.support_proactive_message = False
config = module.MainAgentBuildConfig(
tool_call_timeout=60,
computer_use_runtime="local",
add_cron_tools=False,
)
req = ProviderRequest(prompt="hello")
req.conversation = MagicMock(persona_id="locked", history="[]")
with (
patch("astrbot.core.astr_main_agent.AgentRunner") as mock_runner_cls,
patch("astrbot.core.astr_main_agent.AstrAgentContext"),
):
mock_runner = MagicMock()
mock_runner.reset = AsyncMock()
mock_runner_cls.return_value = mock_runner
result = await module.build_main_agent(
event=mock_event,
plugin_context=mock_context,
config=config,
provider=mock_provider,
req=req,
apply_reset=False,
)
assert result is not None
try:
assert result.provider_request.func_tool is not None
tool_names = result.provider_request.func_tool.names()
assert "astrbot_execute_shell" in tool_names
assert "astrbot_execute_python" in tool_names
finally:
if result.reset_coro:
result.reset_coro.close()