Skip to content

Commit 6334718

Browse files
committed
fix: clear unavailable sandbox booter config
1 parent 288baf2 commit 6334718

2 files changed

Lines changed: 99 additions & 3 deletions

File tree

astrbot/dashboard/routes/config.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,22 @@ def _protected_2fa_config_changed(old_config: dict, new_config: dict) -> bool:
285285
)
286286

287287

288+
def _normalize_unavailable_sandbox_booter(config: dict) -> dict:
289+
sandbox = config.get("provider_settings", {}).get("sandbox", {})
290+
if not isinstance(sandbox, dict):
291+
return config
292+
booter = str(sandbox.get("booter") or "").strip()
293+
if not booter:
294+
return config
295+
provider_ids = {
296+
str(provider.get("provider_id") or "")
297+
for provider in computer_client.list_sandbox_providers()
298+
}
299+
if booter not in provider_ids:
300+
sandbox["booter"] = ""
301+
return config
302+
303+
288304
async def _validate_neo_connectivity(
289305
post_config: dict,
290306
) -> str | None:
@@ -629,7 +645,8 @@ async def get_default_config(self):
629645
metadata = ConfigMetadataI18n.convert_to_i18n_keys(
630646
self._inject_sandbox_provider_options(copy.deepcopy(CONFIG_METADATA_3))
631647
)
632-
return Response().ok({"config": DEFAULT_CONFIG, "metadata": metadata}).__dict__
648+
config = _normalize_unavailable_sandbox_booter(copy.deepcopy(DEFAULT_CONFIG))
649+
return Response().ok({"config": config, "metadata": metadata}).__dict__
633650

634651
async def get_abconf_list(self):
635652
"""获取所有 AstrBot 配置文件的列表"""
@@ -660,7 +677,9 @@ async def get_abconf(self):
660677

661678
try:
662679
if system_config:
663-
abconf = self.acm.confs["default"]
680+
abconf = _normalize_unavailable_sandbox_booter(
681+
copy.deepcopy(dict(self.acm.confs["default"]))
682+
)
664683
metadata = ConfigMetadataI18n.convert_to_i18n_keys(
665684
self._inject_sandbox_provider_options(
666685
copy.deepcopy(CONFIG_METADATA_3_SYSTEM)
@@ -669,7 +688,9 @@ async def get_abconf(self):
669688
return Response().ok({"config": abconf, "metadata": metadata}).__dict__
670689
if abconf_id is None:
671690
raise ValueError("abconf_id cannot be None")
672-
abconf = self.acm.confs[abconf_id]
691+
abconf = _normalize_unavailable_sandbox_booter(
692+
copy.deepcopy(dict(self.acm.confs[abconf_id]))
693+
)
673694
metadata = ConfigMetadataI18n.convert_to_i18n_keys(
674695
self._inject_sandbox_provider_options(copy.deepcopy(CONFIG_METADATA_3))
675696
)
@@ -1086,6 +1107,8 @@ async def post_astrbot_configs(self):
10861107
config, ("dashboard", "totp", "recovery_code_hash"), ""
10871108
)
10881109

1110+
_normalize_unavailable_sandbox_booter(config)
1111+
10891112
set_pending_totp_secret(None)
10901113
await self._save_astrbot_configs(config, conf_id)
10911114
if protected_2fa_changed:

tests/test_dashboard.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,79 @@ async def test_config_metadata_includes_registered_sandbox_providers(
480480
assert "dashboard-generic" in metadata_text
481481

482482

483+
@pytest.mark.asyncio
484+
async def test_config_abconf_clears_unavailable_sandbox_booter_for_display(
485+
app: Quart,
486+
authenticated_header: dict,
487+
monkeypatch: pytest.MonkeyPatch,
488+
core_lifecycle_td: AstrBotCoreLifecycle,
489+
):
490+
from astrbot.core.computer import computer_client
491+
from astrbot.core.computer.sandbox_manager import SandboxManager
492+
from astrbot.core.computer.sandbox_registry import SandboxRegistry
493+
494+
manager = SandboxManager(registry=SandboxRegistry(), providers={})
495+
monkeypatch.setattr(computer_client, "sandbox_manager", manager)
496+
original_booter = core_lifecycle_td.astrbot_config["provider_settings"][
497+
"sandbox"
498+
].get("booter")
499+
core_lifecycle_td.astrbot_config["provider_settings"]["sandbox"]["booter"] = (
500+
"shipyard"
501+
)
502+
503+
try:
504+
test_client = app.test_client()
505+
response = await test_client.get(
506+
"/api/config/abconf?id=default", headers=authenticated_header
507+
)
508+
data = await response.get_json()
509+
510+
assert response.status_code == 200
511+
assert data["status"] == "ok"
512+
assert data["data"]["config"]["provider_settings"]["sandbox"]["booter"] == ""
513+
finally:
514+
core_lifecycle_td.astrbot_config["provider_settings"]["sandbox"]["booter"] = (
515+
original_booter
516+
)
517+
518+
519+
@pytest.mark.asyncio
520+
async def test_config_save_clears_unavailable_sandbox_booter(
521+
app: Quart,
522+
authenticated_header: dict,
523+
monkeypatch: pytest.MonkeyPatch,
524+
core_lifecycle_td: AstrBotCoreLifecycle,
525+
):
526+
from astrbot.core.computer import computer_client
527+
from astrbot.core.computer.sandbox_manager import SandboxManager
528+
from astrbot.core.computer.sandbox_registry import SandboxRegistry
529+
530+
manager = SandboxManager(registry=SandboxRegistry(), providers={})
531+
monkeypatch.setattr(computer_client, "sandbox_manager", manager)
532+
original_config = copy.deepcopy(dict(core_lifecycle_td.astrbot_config))
533+
post_config = copy.deepcopy(original_config)
534+
post_config["provider_settings"]["computer_use_runtime"] = "sandbox"
535+
post_config["provider_settings"]["sandbox"]["booter"] = "shipyard"
536+
537+
try:
538+
test_client = app.test_client()
539+
response = await test_client.post(
540+
"/api/config/astrbot/update",
541+
headers=authenticated_header,
542+
json={"conf_id": "default", "config": post_config},
543+
)
544+
data = await response.get_json()
545+
546+
assert response.status_code == 200
547+
assert data["status"] == "ok"
548+
assert (
549+
core_lifecycle_td.astrbot_config["provider_settings"]["sandbox"]["booter"]
550+
== ""
551+
)
552+
finally:
553+
core_lifecycle_td.astrbot_config.save_config(original_config)
554+
555+
483556
@pytest.mark.asyncio
484557
async def test_sandbox_dashboard_lists_managed_sandboxes(
485558
app: Quart,

0 commit comments

Comments
 (0)