From afe292de350c3c04a72d697acb5d11e4a6b991f3 Mon Sep 17 00:00:00 2001 From: zenfun Date: Wed, 11 Feb 2026 19:35:01 +0800 Subject: [PATCH] fix: address neo skill review findings --- astrbot/core/computer/computer_client.py | 16 ++++++++------ astrbot/dashboard/routes/skills.py | 7 ++++--- .../components/extension/SkillsSection.vue | 21 +++++++++++++++---- tests/test_dashboard.py | 10 +++++++++ 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/astrbot/core/computer/computer_client.py b/astrbot/core/computer/computer_client.py index 0aee0eb09..fed81ab22 100644 --- a/astrbot/core/computer/computer_client.py +++ b/astrbot/core/computer/computer_client.py @@ -40,15 +40,19 @@ async def _sync_skills_to_sandbox(booter: ComputerBooter) -> None: upload_result = await booter.upload_file(zip_path, str(remote_zip)) if not upload_result.get("success", False): raise RuntimeError("Failed to upload skills bundle to sandbox.") - # Always overwrite with local source of truth to avoid stale skills in long-lived sandboxes. + # Extract into a temp folder first, then replace skills atomically. + tmp_extract_root = f"{SANDBOX_SKILLS_ROOT}_tmp_extract" await booter.shell.exec( - f"mkdir -p {SANDBOX_SKILLS_ROOT} && " - f"rm -rf {SANDBOX_SKILLS_ROOT}/* && " - f"(unzip -o {remote_zip} -d {SANDBOX_SKILLS_ROOT} || " + f"mkdir -p {SANDBOX_SKILLS_ROOT} {tmp_extract_root} && " + f"rm -rf {tmp_extract_root}/* && " + f"(unzip -o {remote_zip} -d {tmp_extract_root} || " f"python3 -c \"import zipfile; z=zipfile.ZipFile('{remote_zip}'); " - f"z.extractall('{SANDBOX_SKILLS_ROOT}')\" || " + f"z.extractall('{tmp_extract_root}')\" || " f"python -c \"import zipfile; z=zipfile.ZipFile('{remote_zip}'); " - f"z.extractall('{SANDBOX_SKILLS_ROOT}')\"); " + f"z.extractall('{tmp_extract_root}')\") && " + f"find {SANDBOX_SKILLS_ROOT} -mindepth 1 -delete && " + f"cp -a {tmp_extract_root}/. {SANDBOX_SKILLS_ROOT}/ && " + f"rm -rf {tmp_extract_root} && " f"rm -f {remote_zip}" ) finally: diff --git a/astrbot/dashboard/routes/skills.py b/astrbot/dashboard/routes/skills.py index e525539dd..9c58a4a7e 100644 --- a/astrbot/dashboard/routes/skills.py +++ b/astrbot/dashboard/routes/skills.py @@ -250,9 +250,10 @@ class SkillsRoute(Route): endpoint, access_token = self._get_neo_client_config() data = await request.get_json() candidate_id = data.get("candidate_id") - passed = data.get("passed") - if not candidate_id or passed is None: + passed_value = data.get("passed") + if not candidate_id or passed_value is None: return Response().error("Missing candidate_id or passed").__dict__ + passed = _to_bool(passed_value, False) from shipyard_neo import BayClient @@ -262,7 +263,7 @@ class SkillsRoute(Route): ) as client: result = await client.skills.evaluate_candidate( candidate_id, - passed=bool(passed), + passed=passed, score=data.get("score"), benchmark_id=data.get("benchmark_id"), report=data.get("report"), diff --git a/dashboard/src/components/extension/SkillsSection.vue b/dashboard/src/components/extension/SkillsSection.vue index f8b532f88..686890247 100644 --- a/dashboard/src/components/extension/SkillsSection.vue +++ b/dashboard/src/components/extension/SkillsSection.vue @@ -311,6 +311,13 @@ export default { return payload.skills || []; }; + const normalizeNeoItemsPayload = (res) => { + const payload = res?.data?.data || []; + if (Array.isArray(payload)) return payload; + if (Array.isArray(payload.items)) return payload.items; + return []; + }; + const fetchSkills = async () => { loading.value = true; try { @@ -406,8 +413,7 @@ export default { status: neoFilters.status || undefined, }; const res = await axios.get("/api/skills/neo/candidates", { params }); - const payload = res?.data?.data || {}; - neoCandidates.value = payload.items || []; + neoCandidates.value = normalizeNeoItemsPayload(res); }; const fetchNeoReleases = async () => { @@ -416,8 +422,15 @@ export default { stage: neoFilters.stage || undefined, }; const res = await axios.get("/api/skills/neo/releases", { params }); - const payload = res?.data?.data || {}; - neoReleases.value = payload.items || []; + neoReleases.value = normalizeNeoItemsPayload(res).map((item) => { + if (!item || typeof item !== "object") { + return item; + } + return { + ...item, + is_active: item.is_active ?? item.active ?? false, + }; + }); }; const fetchNeoData = async () => { diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 3b7d46fd4..07c2d8698 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -385,6 +385,16 @@ async def test_neo_skills_routes( assert data["data"]["candidate_id"] == "cand-1" assert data["data"]["passed"] is True + response = await test_client.post( + "/api/skills/neo/evaluate", + json={"candidate_id": "cand-1", "passed": "false", "score": 0.0}, + headers=authenticated_header, + ) + assert response.status_code == 200 + data = await response.get_json() + assert data["status"] == "ok" + assert data["data"]["passed"] is False + response = await test_client.post( "/api/skills/neo/promote", json={"candidate_id": "cand-1", "stage": "stable"},