From 0b77fc33691726e972a3f77b7ff97eb43ac981de Mon Sep 17 00:00:00 2001 From: VectorPeak <73048950+VectorPeak@users.noreply.github.com> Date: Sun, 12 Jul 2026 10:52:24 +0800 Subject: [PATCH 1/3] fix: return HTTP errors for failed skill downloads Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com> --- astrbot/dashboard/api/skills.py | 8 +++--- tests/test_fastapi_v1_dashboard.py | 41 +++++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/astrbot/dashboard/api/skills.py b/astrbot/dashboard/api/skills.py index c8925c8e4a..6e09fd69e6 100644 --- a/astrbot/dashboard/api/skills.py +++ b/astrbot/dashboard/api/skills.py @@ -2,7 +2,7 @@ from typing import Any -from fastapi import APIRouter, Depends, Request +from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import FileResponse from astrbot.core import logger @@ -94,10 +94,12 @@ async def _download_skill(service: SkillsService, name: str): try: return _archive_response(service.prepare_skill_archive(name)) except SkillsServiceError as exc: - return error(str(exc)) + message = str(exc) + status_code = 404 if message == "Local skill not found" else 400 + raise HTTPException(status_code=status_code, detail=message) from exc except Exception as exc: logger.error(str(exc), exc_info=True) - return error(str(exc)) + raise HTTPException(status_code=500, detail=str(exc)) from exc @router.get("/skills") diff --git a/tests/test_fastapi_v1_dashboard.py b/tests/test_fastapi_v1_dashboard.py index 7c0342c539..97171a7cfb 100644 --- a/tests/test_fastapi_v1_dashboard.py +++ b/tests/test_fastapi_v1_dashboard.py @@ -28,7 +28,7 @@ PLUGIN_UPDATE_SOURCE_REQUIRED_MESSAGE, PluginServiceError, ) -from astrbot.dashboard.services.skills_service import SkillArchive +from astrbot.dashboard.services.skills_service import SkillArchive, SkillsServiceError JWT_SECRET = "fastapi-v1-test-secret-with-32-bytes" @@ -1215,8 +1215,7 @@ async def test_v1_knowledge_base_create_validation_uses_api_error_shape( assert missing_provider_response.status_code == 200 assert missing_provider_response.json()["status"] == "error" assert ( - missing_provider_response.json()["message"] - == "缺少参数 embedding_provider_id" + missing_provider_response.json()["message"] == "缺少参数 embedding_provider_id" ) @@ -3227,6 +3226,42 @@ async def fake_update_skill_file(data): assert delete_response.json()["data"]["payload"] == {"name": skill_name} +@pytest.mark.asyncio +async def test_v1_skill_archive_errors_return_http_status( + asgi_app: FastAPI, + asgi_client: httpx.AsyncClient, + monkeypatch: pytest.MonkeyPatch, +): + skill_service = asgi_app.state.services.skills + + def fake_prepare_skill_archive(_name: str): + raise SkillsServiceError("Local skill not found") + + monkeypatch.setattr( + skill_service, + "prepare_skill_archive", + fake_prepare_skill_archive, + ) + + by_name_response = await asgi_client.get( + "/api/v1/skills/archive", + params={"skill_name": "missing_skill"}, + headers=_jwt_headers(), + ) + path_response = await asgi_client.get( + "/api/v1/skills/missing_skill/archive", + headers=_jwt_headers(), + ) + + assert by_name_response.status_code == 404 + assert by_name_response.headers["content-type"].startswith("application/json") + assert by_name_response.json()["status"] == "error" + assert by_name_response.json()["message"] == "Local skill not found" + assert path_response.status_code == 404 + assert path_response.headers["content-type"].startswith("application/json") + assert path_response.json()["message"] == "Local skill not found" + + @pytest.mark.asyncio async def test_v1_safe_persona_routes_accept_slash_ids( asgi_client: httpx.AsyncClient, From b11eb0dd5184a5b33f680d41f5b7131f02ffe79d Mon Sep 17 00:00:00 2001 From: VectorPeak <73048950+VectorPeak@users.noreply.github.com> Date: Sun, 12 Jul 2026 11:02:43 +0800 Subject: [PATCH 2/3] fix: harden skill archive error responses Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com> --- astrbot/dashboard/api/skills.py | 7 +++-- tests/test_fastapi_v1_dashboard.py | 45 ++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/astrbot/dashboard/api/skills.py b/astrbot/dashboard/api/skills.py index 6e09fd69e6..cd4e9ec610 100644 --- a/astrbot/dashboard/api/skills.py +++ b/astrbot/dashboard/api/skills.py @@ -95,11 +95,14 @@ async def _download_skill(service: SkillsService, name: str): return _archive_response(service.prepare_skill_archive(name)) except SkillsServiceError as exc: message = str(exc) - status_code = 404 if message == "Local skill not found" else 400 + status_code = 404 if "not found" in message.lower() else 400 raise HTTPException(status_code=status_code, detail=message) from exc except Exception as exc: logger.error(str(exc), exc_info=True) - raise HTTPException(status_code=500, detail=str(exc)) from exc + raise HTTPException( + status_code=500, + detail="Failed to prepare skill archive", + ) from exc @router.get("/skills") diff --git a/tests/test_fastapi_v1_dashboard.py b/tests/test_fastapi_v1_dashboard.py index 97171a7cfb..11e0ccb2b5 100644 --- a/tests/test_fastapi_v1_dashboard.py +++ b/tests/test_fastapi_v1_dashboard.py @@ -3234,13 +3234,13 @@ async def test_v1_skill_archive_errors_return_http_status( ): skill_service = asgi_app.state.services.skills - def fake_prepare_skill_archive(_name: str): + def fake_prepare_skill_archive_404(_name: str): raise SkillsServiceError("Local skill not found") monkeypatch.setattr( skill_service, "prepare_skill_archive", - fake_prepare_skill_archive, + fake_prepare_skill_archive_404, ) by_name_response = await asgi_client.get( @@ -3261,6 +3261,47 @@ def fake_prepare_skill_archive(_name: str): assert path_response.headers["content-type"].startswith("application/json") assert path_response.json()["message"] == "Local skill not found" + def fake_prepare_skill_archive_400(_name: str): + raise SkillsServiceError("Invalid skill name") + + monkeypatch.setattr( + skill_service, + "prepare_skill_archive", + fake_prepare_skill_archive_400, + ) + + bad_request_response = await asgi_client.get( + "/api/v1/skills/archive", + params={"skill_name": "invalid_skill"}, + headers=_jwt_headers(), + ) + + assert bad_request_response.status_code == 400 + assert bad_request_response.headers["content-type"].startswith("application/json") + assert bad_request_response.json()["status"] == "error" + assert bad_request_response.json()["message"] == "Invalid skill name" + + def fake_prepare_skill_archive_500(_name: str): + raise RuntimeError("Unexpected database error") + + monkeypatch.setattr( + skill_service, + "prepare_skill_archive", + fake_prepare_skill_archive_500, + ) + + server_error_response = await asgi_client.get( + "/api/v1/skills/archive", + params={"skill_name": "error_skill"}, + headers=_jwt_headers(), + ) + + assert server_error_response.status_code == 500 + assert server_error_response.headers["content-type"].startswith("application/json") + assert server_error_response.json()["status"] == "error" + assert server_error_response.json()["message"] == "Failed to prepare skill archive" + assert "Unexpected database error" not in server_error_response.text + @pytest.mark.asyncio async def test_v1_safe_persona_routes_accept_slash_ids( From 3314d1e980b89057ff748c65f63f5eedc0630e68 Mon Sep 17 00:00:00 2001 From: VectorPeak <73048950+VectorPeak@users.noreply.github.com> Date: Sun, 12 Jul 2026 11:15:58 +0800 Subject: [PATCH 3/3] fix: use structured skill archive status codes Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com> --- astrbot/dashboard/api/skills.py | 3 +-- astrbot/dashboard/services/skills_service.py | 6 ++++-- tests/test_fastapi_v1_dashboard.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/astrbot/dashboard/api/skills.py b/astrbot/dashboard/api/skills.py index cd4e9ec610..ebc784a71c 100644 --- a/astrbot/dashboard/api/skills.py +++ b/astrbot/dashboard/api/skills.py @@ -95,8 +95,7 @@ async def _download_skill(service: SkillsService, name: str): return _archive_response(service.prepare_skill_archive(name)) except SkillsServiceError as exc: message = str(exc) - status_code = 404 if "not found" in message.lower() else 400 - raise HTTPException(status_code=status_code, detail=message) from exc + raise HTTPException(status_code=exc.status_code, detail=message) from exc except Exception as exc: logger.error(str(exc), exc_info=True) raise HTTPException( diff --git a/astrbot/dashboard/services/skills_service.py b/astrbot/dashboard/services/skills_service.py index 31c5c9afd0..4bcc6f8810 100644 --- a/astrbot/dashboard/services/skills_service.py +++ b/astrbot/dashboard/services/skills_service.py @@ -39,7 +39,9 @@ class SkillsServiceError(Exception): - pass + def __init__(self, message: str, *, status_code: int = 400) -> None: + super().__init__(message) + self.status_code = status_code @dataclass @@ -436,7 +438,7 @@ def prepare_skill_archive(self, name: str) -> SkillArchive: skill_dir = Path(skill_mgr.skills_root) / skill_name skill_md = skill_dir / "SKILL.md" if not skill_dir.is_dir() or not skill_md.exists(): - raise SkillsServiceError("Local skill not found") + raise SkillsServiceError("Local skill not found", status_code=404) export_dir = Path(get_astrbot_temp_path()) / "skill_exports" export_dir.mkdir(parents=True, exist_ok=True) diff --git a/tests/test_fastapi_v1_dashboard.py b/tests/test_fastapi_v1_dashboard.py index 11e0ccb2b5..f129c1e8de 100644 --- a/tests/test_fastapi_v1_dashboard.py +++ b/tests/test_fastapi_v1_dashboard.py @@ -3235,7 +3235,7 @@ async def test_v1_skill_archive_errors_return_http_status( skill_service = asgi_app.state.services.skills def fake_prepare_skill_archive_404(_name: str): - raise SkillsServiceError("Local skill not found") + raise SkillsServiceError("Local skill not found", status_code=404) monkeypatch.setattr( skill_service,