From caab251f023530ddefa8ca19756d9992e50fbf52 Mon Sep 17 00:00:00 2001 From: supreme0597 Date: Mon, 25 May 2026 23:35:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(skill):=20=E5=BC=82=E6=AD=A5=E5=8C=96?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=9B=AE=E5=BD=95=E9=98=B2=E9=98=BB=E5=A1=9E?= =?UTF-8?q?=E5=B9=B6=E9=99=90=E5=88=B6=E6=89=B9=E9=87=8F=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E4=B8=8A=E9=99=90=E4=BB=A5=E9=81=BF=E5=85=8D=E8=B6=85=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/remote_skill_install_service.py | 14 +++---- .../package/yuxi/services/skill_service.py | 14 ++++--- backend/server/routers/skill_router.py | 2 +- .../test/unit/services/test_skill_service.py | 7 ++++ web/src/components/AgentChatComponent.vue | 8 ++-- web/src/components/common/MarkdownPreview.vue | 11 ++--- .../components/extensions/SkillCardList.vue | 40 +++++++++++-------- web/src/utils/__tests__/svgRenderer.test.js | 7 +++- web/src/utils/svgRenderer.js | 10 ++--- 9 files changed, 67 insertions(+), 46 deletions(-) diff --git a/backend/package/yuxi/services/remote_skill_install_service.py b/backend/package/yuxi/services/remote_skill_install_service.py index e5f8fa6f..c8780035 100644 --- a/backend/package/yuxi/services/remote_skill_install_service.py +++ b/backend/package/yuxi/services/remote_skill_install_service.py @@ -25,9 +25,9 @@ class RemoteSkillsBatchPreparation: temp_home: str | None results: list[dict] - def cleanup(self) -> None: + async def cleanup(self) -> None: if self.temp_home: - shutil.rmtree(self.temp_home, ignore_errors=True) + await asyncio.to_thread(shutil.rmtree, self.temp_home, ignore_errors=True) def _normalize_source(source: str) -> str: @@ -152,7 +152,7 @@ async def list_remote_skills(source: str) -> list[dict[str, str]]: cwd=workdir, ) finally: - shutil.rmtree(temp_home, ignore_errors=True) + await asyncio.to_thread(shutil.rmtree, temp_home, ignore_errors=True) skills = _parse_available_skills(output) if not skills: @@ -219,7 +219,7 @@ async def install_remote_skill( created_by=created_by, ) finally: - shutil.rmtree(temp_home, ignore_errors=True) + await asyncio.to_thread(shutil.rmtree, temp_home, ignore_errors=True) async def install_remote_skills_batch( @@ -262,7 +262,7 @@ async def install_remote_skills_batch( return results finally: - preparation.cleanup() + await preparation.cleanup() async def prepare_remote_skills_batch( @@ -327,7 +327,7 @@ async def prepare_remote_skills_batch( return RemoteSkillsBatchPreparation(temp_home=temp_home, results=results) except Exception: - shutil.rmtree(temp_home, ignore_errors=True) + await asyncio.to_thread(shutil.rmtree, temp_home, ignore_errors=True) raise @@ -382,6 +382,6 @@ async def search_remote_skills(query: str) -> list[dict[str, str]]: cwd=workdir, ) finally: - shutil.rmtree(temp_home, ignore_errors=True) + await asyncio.to_thread(shutil.rmtree, temp_home, ignore_errors=True) return _parse_search_skills(output) diff --git a/backend/package/yuxi/services/skill_service.py b/backend/package/yuxi/services/skill_service.py index 34fb5958..a6663a63 100644 --- a/backend/package/yuxi/services/skill_service.py +++ b/backend/package/yuxi/services/skill_service.py @@ -459,12 +459,12 @@ async def _import_skill_dir_impl( temp_target = skills_root / f".{final_slug}.tmp-{uuid.uuid4().hex[:8]}" if temp_target.exists(): - shutil.rmtree(temp_target) + await asyncio.to_thread(shutil.rmtree, temp_target) shutil.move(str(stage_dir), str(temp_target)) final_dir = skills_root / final_slug if final_dir.exists(): - shutil.rmtree(temp_target, ignore_errors=True) + await asyncio.to_thread(shutil.rmtree, temp_target, ignore_errors=True) raise ValueError(f"技能目录冲突,请重试: {final_slug}") temp_target.rename(final_dir) @@ -480,7 +480,7 @@ async def _import_skill_dir_impl( created_by=created_by, ) except Exception: - shutil.rmtree(final_dir, ignore_errors=True) + await asyncio.to_thread(shutil.rmtree, final_dir, ignore_errors=True) raise return item @@ -728,7 +728,7 @@ async def delete_skill_node(db: AsyncSession, *, slug: str, relative_path: str) raise ValueError("不允许删除根目录 SKILL.md") if target.is_dir(): - shutil.rmtree(target) + await asyncio.to_thread(shutil.rmtree, target) else: target.unlink() @@ -774,11 +774,13 @@ async def delete_skill(db: AsyncSession, *, slug: str) -> None: raise if trash_dir and trash_dir.exists(): - shutil.rmtree(trash_dir, ignore_errors=True) + await asyncio.to_thread(shutil.rmtree, trash_dir, ignore_errors=True) async def delete_skills_batch(db: AsyncSession, *, slugs: list[str]) -> list[dict]: """批量删除多个 skills(单技能独立的子事务与回滚)。""" + if len(slugs) > 50: + raise ValueError("批量删除的技能数量不能超过 50 个") results = [] for slug in slugs: try: @@ -897,7 +899,7 @@ async def install_builtin_skill(db: AsyncSession, slug: str, *, installed_by: st created_by=installed_by or BUILTIN_SKILL_OPERATOR, ) except Exception: - shutil.rmtree(target_dir, ignore_errors=True) + await asyncio.to_thread(shutil.rmtree, target_dir, ignore_errors=True) raise diff --git a/backend/server/routers/skill_router.py b/backend/server/routers/skill_router.py index 75e14e79..ef3c5a20 100644 --- a/backend/server/routers/skill_router.py +++ b/backend/server/routers/skill_router.py @@ -77,7 +77,7 @@ class RemoteSkillSearchRequest(BaseModel): class SkillBatchDeleteRequest(BaseModel): - slugs: list[str] = Field(..., description="需要批量删除的 skill slug 列表") + slugs: list[str] = Field(..., max_length=50, description="需要批量删除的 skill slug 列表,最多支持 50 个") def _raise_from_value_error(e: ValueError) -> None: diff --git a/backend/test/unit/services/test_skill_service.py b/backend/test/unit/services/test_skill_service.py index 3d99e8fb..d7e8f67a 100644 --- a/backend/test/unit/services/test_skill_service.py +++ b/backend/test/unit/services/test_skill_service.py @@ -1053,6 +1053,13 @@ async def test_delete_skills_batch_ok(tmp_path: Path, monkeypatch: pytest.Monkey assert not (tmp_path / "skills" / "skill-b").exists() +@pytest.mark.asyncio +async def test_delete_skills_batch_limit_exceeded(): + slugs = [f"skill-{i}" for i in range(51)] + with pytest.raises(ValueError, match="批量删除的技能数量不能超过 50 个"): + await svc.delete_skills_batch(None, slugs=slugs) + + @pytest.mark.asyncio async def test_delete_skill_concurrent_lock(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr(svc.sys_config, "save_dir", str(tmp_path)) diff --git a/web/src/components/AgentChatComponent.vue b/web/src/components/AgentChatComponent.vue index 2c78e1ca..48d9c59d 100644 --- a/web/src/components/AgentChatComponent.vue +++ b/web/src/components/AgentChatComponent.vue @@ -1614,10 +1614,10 @@ const hasVisibleAssistantBody = (message) => { const { content, reasoningContent } = extractAssistantMessageBody(message) return Boolean( content || - reasoningContent || - message.error_type || - message.extra_metadata?.error_type || - message.isStoppedByUser + reasoningContent || + message.error_type || + message.extra_metadata?.error_type || + message.isStoppedByUser ) } diff --git a/web/src/components/common/MarkdownPreview.vue b/web/src/components/common/MarkdownPreview.vue index c0091d85..6a463b0b 100644 --- a/web/src/components/common/MarkdownPreview.vue +++ b/web/src/components/common/MarkdownPreview.vue @@ -100,7 +100,10 @@ const copySvgAsPng = async (svgEl, btn) => { } // 3) 回退 - if (!width || !height) { width = 800; height = 600 } + if (!width || !height) { + width = 800 + height = 600 + } const img = await new Promise((resolve, reject) => { const image = new Image() @@ -117,11 +120,9 @@ const copySvgAsPng = async (svgEl, btn) => { // 背景色由 SVG 自身决定 ctx.drawImage(img, 0, 0, width, height) - const pngBlob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png')) + const pngBlob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png')) if (pngBlob) { - await navigator.clipboard.write([ - new ClipboardItem({ 'image/png': pngBlob }) - ]) + await navigator.clipboard.write([new ClipboardItem({ 'image/png': pngBlob })]) showCopiedFeedback(btn) } } catch (err) { diff --git a/web/src/components/extensions/SkillCardList.vue b/web/src/components/extensions/SkillCardList.vue index 8acbd532..62eaf719 100644 --- a/web/src/components/extensions/SkillCardList.vue +++ b/web/src/components/extensions/SkillCardList.vue @@ -141,7 +141,11 @@ :disabled="installingRemoteSkill" >