diff --git a/nanobot/webui/skills_api.py b/nanobot/webui/skills_api.py index 819723c9f..68de970be 100644 --- a/nanobot/webui/skills_api.py +++ b/nanobot/webui/skills_api.py @@ -10,6 +10,7 @@ from typing import Any from nanobot.agent.skills import SkillsLoader from nanobot.config.loader import load_config, save_config +from nanobot.security.workspace_policy import WorkspaceBoundaryError, require_path_within class SkillManagementError(Exception): @@ -100,7 +101,15 @@ def delete_webui_skill( if entry.get("source") != "workspace": raise SkillManagementError("built-in skills cannot be deleted", status=403) - skills_root = (workspace_path.expanduser().resolve() / "skills").resolve() + workspace = workspace_path.expanduser().resolve() + try: + skills_root = require_path_within( + workspace / "skills", + workspace, + message="skills directory must stay inside the workspace", + ) + except WorkspaceBoundaryError as exc: + raise SkillManagementError(str(exc), status=403) from exc target = skills_root / name if target.parent != skills_root: raise SkillManagementError("invalid skill name") diff --git a/nanobot/webui/skills_marketplace.py b/nanobot/webui/skills_marketplace.py index 020e36058..3dea4976d 100644 --- a/nanobot/webui/skills_marketplace.py +++ b/nanobot/webui/skills_marketplace.py @@ -19,6 +19,7 @@ import httpx from nanobot.agent.skills import SkillsLoader from nanobot.security.network import PinnedDNSAsyncTransport +from nanobot.security.workspace_policy import WorkspaceBoundaryError, require_path_within _PROVIDER_ALL = "all" _PROVIDER_SKILLS_SH = "skills_sh" @@ -353,6 +354,17 @@ async def _install_skills_sh_skill( if skill_id in existing: return {"installed": True, "already_installed": True, "name": skill_id} + workspace = workspace_path.expanduser().resolve() + workspace.mkdir(parents=True, exist_ok=True) + try: + require_path_within( + workspace / "skills", + workspace, + message="skills directory must stay inside the workspace", + ) + except WorkspaceBoundaryError as exc: + raise SkillsMarketplaceError(str(exc), status=403) from exc + npx = shutil.which("npx") if npx is None: raise SkillsMarketplaceError( @@ -360,8 +372,6 @@ async def _install_skills_sh_skill( status=503, ) - workspace = workspace_path.expanduser().resolve() - workspace.mkdir(parents=True, exist_ok=True) env = os.environ.copy() env["DISABLE_TELEMETRY"] = "1" command = ( @@ -439,7 +449,15 @@ async def _install_skillhub_skill( } workspace = workspace_path.expanduser().resolve() - skills_root = workspace / "skills" + workspace.mkdir(parents=True, exist_ok=True) + try: + skills_root = require_path_within( + workspace / "skills", + workspace, + message="skills directory must stay inside the workspace", + ) + except WorkspaceBoundaryError as exc: + raise SkillsMarketplaceError(str(exc), status=403) from exc skills_root.mkdir(parents=True, exist_ok=True) target = skills_root / skill_id diff --git a/tests/webui/test_skills_api.py b/tests/webui/test_skills_api.py index 07fbe508d..f69c685f0 100644 --- a/tests/webui/test_skills_api.py +++ b/tests/webui/test_skills_api.py @@ -126,3 +126,27 @@ def test_delete_webui_skill_only_deletes_workspace_skills( with pytest.raises(SkillManagementError) as exc_info: delete_webui_skill(tmp_path, "cron", disabled_skills=disabled) assert exc_info.value.status == 403 + + +def test_delete_webui_skill_rejects_symlinked_skills_root( + tmp_path: Path, +) -> None: + workspace = tmp_path / "workspace" + outside = tmp_path / "outside" + workspace.mkdir() + directory = outside / "custom-skill" + directory.mkdir(parents=True) + (directory / "SKILL.md").write_text( + "---\nname: custom-skill\n---\n", + encoding="utf-8", + ) + try: + (workspace / "skills").symlink_to(outside, target_is_directory=True) + except OSError as exc: + pytest.skip(f"directory symlink unavailable: {exc}") + + with pytest.raises(SkillManagementError) as exc_info: + delete_webui_skill(workspace, "custom-skill", disabled_skills=set()) + + assert exc_info.value.status == 403 + assert (outside / "custom-skill" / "SKILL.md").is_file() diff --git a/tests/webui/test_skills_marketplace.py b/tests/webui/test_skills_marketplace.py index 8318e3f15..993ffbc0d 100644 --- a/tests/webui/test_skills_marketplace.py +++ b/tests/webui/test_skills_marketplace.py @@ -534,3 +534,29 @@ async def test_install_marketplace_skill_is_idempotent( "already_installed": True, "name": "already-here", } + + +@pytest.mark.asyncio +async def test_install_marketplace_skill_rejects_symlinked_skills_root( + tmp_path: Path, +) -> None: + workspace = tmp_path / "workspace" + outside = tmp_path / "outside" + workspace.mkdir() + outside.mkdir() + try: + (workspace / "skills").symlink_to(outside, target_is_directory=True) + except OSError as exc: + pytest.skip(f"directory symlink unavailable: {exc}") + + with pytest.raises(SkillsMarketplaceError) as exc_info: + await install_marketplace_skill( + "", + "ima-skills", + workspace, + provider="skillhub", + version="1.1.8", + ) + + assert exc_info.value.status == 403 + assert list(outside.iterdir()) == [] diff --git a/webui/src/components/settings/SkillsCatalogSettings.tsx b/webui/src/components/settings/SkillsCatalogSettings.tsx index f525f1b61..022b5e76b 100644 --- a/webui/src/components/settings/SkillsCatalogSettings.tsx +++ b/webui/src/components/settings/SkillsCatalogSettings.tsx @@ -485,10 +485,17 @@ function SkillDetailSheet({ type="button" role="switch" aria-checked={enabled} - aria-label={t("settings.skills.toggleEnabled", { - name: activeSkill.name, - defaultValue: enabled ? "Disable {{name}}" : "Enable {{name}}", - })} + aria-label={ + enabled + ? t("settings.skills.disableSkill", { + name: activeSkill.name, + defaultValue: "Disable {{name}}", + }) + : t("settings.skills.enableSkill", { + name: activeSkill.name, + defaultValue: "Enable {{name}}", + }) + } disabled={actionBusy} onClick={() => void toggleEnabled()} className={cn( diff --git a/webui/src/i18n/locales/en/common.json b/webui/src/i18n/locales/en/common.json index 2a9cce992..0eb61895c 100644 --- a/webui/src/i18n/locales/en/common.json +++ b/webui/src/i18n/locales/en/common.json @@ -791,6 +791,36 @@ "views": "Skills views", "installedTab": "Installed", "discoverTab": "Discover", + "customGroup": "Custom", + "builtinGroup": "Built-in", + "otherGroup": "Other", + "searchInstalled": "Search installed skills", + "filterAll": "All", + "filterEnabled": "Enabled", + "filterDisabled": "Disabled", + "noMatching": "No matching skills.", + "statusDisabled": "Disabled", + "statusEnabled": "Enabled", + "statusNeedsSetup": "Needs setup", + "showLess": "Show less", + "showMore": "Show more", + "enabledControl": "Use this skill", + "enabledDescription": "Allow the agent to load this skill when its requirements are ready.", + "enableSkill": "Enable {{name}}", + "disableSkill": "Disable {{name}}", + "updateFailed": "Could not update this skill.", + "deleteTitle": "Delete skill", + "deleteDescription": "Remove this skill from the current workspace.", + "deleteAction": "Delete", + "deleteFailed": "Could not delete this skill.", + "deleteConfirmTitle": "Delete {{name}}?", + "deleteConfirmDescription": "This removes the skill files from the current workspace. This action cannot be undone.", + "deleteConfirmAction": "Delete skill", + "instructionsTitle": "Skill instructions", + "setupRequired": "Setup required", + "setupDescription": "Install the missing dependency on the machine running nanobot, then check again.", + "copySetupCommand": "Copy setup command", + "checkAgain": "Check again", "marketplaceSearchFailed": "Could not search skill marketplaces.", "marketplaceInstallFailed": "Could not install this skill.", "marketplaceSearchPlaceholder": "Search skills", diff --git a/webui/src/i18n/locales/es/common.json b/webui/src/i18n/locales/es/common.json index a40318cc3..f35a74f51 100644 --- a/webui/src/i18n/locales/es/common.json +++ b/webui/src/i18n/locales/es/common.json @@ -778,6 +778,36 @@ "views": "Vistas de habilidades", "installedTab": "Instaladas", "discoverTab": "Descubrir", + "customGroup": "Personalizadas", + "builtinGroup": "Integradas", + "otherGroup": "Otras", + "searchInstalled": "Buscar skills instaladas", + "filterAll": "Todas", + "filterEnabled": "Activadas", + "filterDisabled": "Desactivadas", + "noMatching": "No hay skills coincidentes.", + "statusDisabled": "Desactivada", + "statusEnabled": "Activada", + "statusNeedsSetup": "Requiere configuración", + "showLess": "Mostrar menos", + "showMore": "Mostrar más", + "enabledControl": "Usar esta skill", + "enabledDescription": "Permite que el agente cargue esta skill cuando sus requisitos estén listos.", + "enableSkill": "Activar {{name}}", + "disableSkill": "Desactivar {{name}}", + "updateFailed": "No se pudo actualizar esta skill.", + "deleteTitle": "Eliminar skill", + "deleteDescription": "Elimina esta skill del espacio de trabajo actual.", + "deleteAction": "Eliminar", + "deleteFailed": "No se pudo eliminar esta skill.", + "deleteConfirmTitle": "¿Eliminar {{name}}?", + "deleteConfirmDescription": "Esto elimina los archivos de la skill del espacio de trabajo actual. Esta acción no se puede deshacer.", + "deleteConfirmAction": "Eliminar skill", + "instructionsTitle": "Instrucciones de la skill", + "setupRequired": "Requiere configuración", + "setupDescription": "Instala la dependencia que falta en el equipo donde se ejecuta nanobot y vuelve a comprobarlo.", + "copySetupCommand": "Copiar comando de configuración", + "checkAgain": "Comprobar de nuevo", "marketplaceSearchFailed": "No se pudieron buscar los mercados de skills.", "marketplaceInstallFailed": "No se pudo instalar este skill.", "marketplaceSearchPlaceholder": "Buscar skills", diff --git a/webui/src/i18n/locales/fr/common.json b/webui/src/i18n/locales/fr/common.json index a9a2cbedb..5fa59a4f1 100644 --- a/webui/src/i18n/locales/fr/common.json +++ b/webui/src/i18n/locales/fr/common.json @@ -777,6 +777,36 @@ "views": "Vues des compétences", "installedTab": "Installées", "discoverTab": "Découvrir", + "customGroup": "Personnalisées", + "builtinGroup": "Intégrées", + "otherGroup": "Autres", + "searchInstalled": "Rechercher les compétences installées", + "filterAll": "Toutes", + "filterEnabled": "Activées", + "filterDisabled": "Désactivées", + "noMatching": "Aucune compétence correspondante.", + "statusDisabled": "Désactivée", + "statusEnabled": "Activée", + "statusNeedsSetup": "Configuration requise", + "showLess": "Afficher moins", + "showMore": "Afficher plus", + "enabledControl": "Utiliser cette compétence", + "enabledDescription": "Autorise l’agent à charger cette compétence lorsque ses prérequis sont satisfaits.", + "enableSkill": "Activer {{name}}", + "disableSkill": "Désactiver {{name}}", + "updateFailed": "Impossible de mettre à jour cette compétence.", + "deleteTitle": "Supprimer la compétence", + "deleteDescription": "Supprime cette compétence de l’espace de travail actuel.", + "deleteAction": "Supprimer", + "deleteFailed": "Impossible de supprimer cette compétence.", + "deleteConfirmTitle": "Supprimer {{name}} ?", + "deleteConfirmDescription": "Cette action supprime les fichiers de la compétence de l’espace de travail actuel et ne peut pas être annulée.", + "deleteConfirmAction": "Supprimer la compétence", + "instructionsTitle": "Instructions de la compétence", + "setupRequired": "Configuration requise", + "setupDescription": "Installez la dépendance manquante sur la machine qui exécute nanobot, puis vérifiez à nouveau.", + "copySetupCommand": "Copier la commande de configuration", + "checkAgain": "Vérifier à nouveau", "marketplaceSearchFailed": "Impossible de rechercher dans les catalogues de compétences.", "marketplaceInstallFailed": "Impossible d’installer cette compétence.", "marketplaceSearchPlaceholder": "Rechercher des compétences", diff --git a/webui/src/i18n/locales/id/common.json b/webui/src/i18n/locales/id/common.json index 693eb51c3..476463209 100644 --- a/webui/src/i18n/locales/id/common.json +++ b/webui/src/i18n/locales/id/common.json @@ -777,6 +777,36 @@ "views": "Tampilan skill", "installedTab": "Terpasang", "discoverTab": "Temukan", + "customGroup": "Kustom", + "builtinGroup": "Bawaan", + "otherGroup": "Lainnya", + "searchInstalled": "Cari skill terpasang", + "filterAll": "Semua", + "filterEnabled": "Aktif", + "filterDisabled": "Nonaktif", + "noMatching": "Tidak ada skill yang cocok.", + "statusDisabled": "Nonaktif", + "statusEnabled": "Aktif", + "statusNeedsSetup": "Perlu penyiapan", + "showLess": "Tampilkan lebih sedikit", + "showMore": "Tampilkan lebih banyak", + "enabledControl": "Gunakan skill ini", + "enabledDescription": "Izinkan agen memuat skill ini saat persyaratannya terpenuhi.", + "enableSkill": "Aktifkan {{name}}", + "disableSkill": "Nonaktifkan {{name}}", + "updateFailed": "Skill ini tidak dapat diperbarui.", + "deleteTitle": "Hapus skill", + "deleteDescription": "Hapus skill ini dari workspace saat ini.", + "deleteAction": "Hapus", + "deleteFailed": "Skill ini tidak dapat dihapus.", + "deleteConfirmTitle": "Hapus {{name}}?", + "deleteConfirmDescription": "Tindakan ini menghapus file skill dari workspace saat ini dan tidak dapat dibatalkan.", + "deleteConfirmAction": "Hapus skill", + "instructionsTitle": "Petunjuk skill", + "setupRequired": "Perlu penyiapan", + "setupDescription": "Instal dependensi yang belum tersedia di mesin yang menjalankan nanobot, lalu periksa lagi.", + "copySetupCommand": "Salin perintah penyiapan", + "checkAgain": "Periksa lagi", "marketplaceSearchFailed": "Tidak dapat mencari marketplace skill.", "marketplaceInstallFailed": "Tidak dapat memasang skill ini.", "marketplaceSearchPlaceholder": "Cari skill", diff --git a/webui/src/i18n/locales/ja/common.json b/webui/src/i18n/locales/ja/common.json index a73db8dc2..9c41549e9 100644 --- a/webui/src/i18n/locales/ja/common.json +++ b/webui/src/i18n/locales/ja/common.json @@ -777,6 +777,36 @@ "views": "スキル表示", "installedTab": "インストール済み", "discoverTab": "見つける", + "customGroup": "カスタム", + "builtinGroup": "組み込み", + "otherGroup": "その他", + "searchInstalled": "インストール済みスキルを検索", + "filterAll": "すべて", + "filterEnabled": "有効", + "filterDisabled": "無効", + "noMatching": "一致するスキルがありません。", + "statusDisabled": "無効", + "statusEnabled": "有効", + "statusNeedsSetup": "セットアップが必要", + "showLess": "折りたたむ", + "showMore": "さらに表示", + "enabledControl": "このスキルを使用", + "enabledDescription": "必要条件が満たされている場合、エージェントがこのスキルを読み込めるようにします。", + "enableSkill": "{{name}} を有効にする", + "disableSkill": "{{name}} を無効にする", + "updateFailed": "このスキルを更新できませんでした。", + "deleteTitle": "スキルを削除", + "deleteDescription": "現在のワークスペースからこのスキルを削除します。", + "deleteAction": "削除", + "deleteFailed": "このスキルを削除できませんでした。", + "deleteConfirmTitle": "{{name}} を削除しますか?", + "deleteConfirmDescription": "現在のワークスペースからスキルファイルを削除します。この操作は元に戻せません。", + "deleteConfirmAction": "スキルを削除", + "instructionsTitle": "スキルの説明", + "setupRequired": "セットアップが必要", + "setupDescription": "nanobot を実行しているマシンに不足している依存関係をインストールしてから、再確認してください。", + "copySetupCommand": "セットアップコマンドをコピー", + "checkAgain": "再確認", "marketplaceSearchFailed": "スキルマーケットを検索できませんでした。", "marketplaceInstallFailed": "このスキルをインストールできませんでした。", "marketplaceSearchPlaceholder": "スキルを検索", diff --git a/webui/src/i18n/locales/ko/common.json b/webui/src/i18n/locales/ko/common.json index db6a29c51..7ab14c1cb 100644 --- a/webui/src/i18n/locales/ko/common.json +++ b/webui/src/i18n/locales/ko/common.json @@ -777,6 +777,36 @@ "views": "스킬 보기", "installedTab": "설치됨", "discoverTab": "탐색", + "customGroup": "사용자 지정", + "builtinGroup": "기본 제공", + "otherGroup": "기타", + "searchInstalled": "설치된 스킬 검색", + "filterAll": "전체", + "filterEnabled": "활성화됨", + "filterDisabled": "비활성화됨", + "noMatching": "일치하는 스킬이 없습니다.", + "statusDisabled": "비활성화됨", + "statusEnabled": "활성화됨", + "statusNeedsSetup": "설정 필요", + "showLess": "접기", + "showMore": "더 보기", + "enabledControl": "이 스킬 사용", + "enabledDescription": "요구 사항이 준비되면 에이전트가 이 스킬을 불러오도록 허용합니다.", + "enableSkill": "{{name}} 활성화", + "disableSkill": "{{name}} 비활성화", + "updateFailed": "이 스킬을 업데이트할 수 없습니다.", + "deleteTitle": "스킬 삭제", + "deleteDescription": "현재 워크스페이스에서 이 스킬을 제거합니다.", + "deleteAction": "삭제", + "deleteFailed": "이 스킬을 삭제할 수 없습니다.", + "deleteConfirmTitle": "{{name}}을(를) 삭제하시겠습니까?", + "deleteConfirmDescription": "현재 워크스페이스에서 스킬 파일을 제거합니다. 이 작업은 되돌릴 수 없습니다.", + "deleteConfirmAction": "스킬 삭제", + "instructionsTitle": "스킬 안내", + "setupRequired": "설정 필요", + "setupDescription": "nanobot을 실행하는 컴퓨터에 누락된 종속성을 설치한 후 다시 확인하세요.", + "copySetupCommand": "설정 명령 복사", + "checkAgain": "다시 확인", "marketplaceSearchFailed": "스킬 마켓을 검색할 수 없습니다.", "marketplaceInstallFailed": "이 스킬을 설치할 수 없습니다.", "marketplaceSearchPlaceholder": "스킬 검색", diff --git a/webui/src/i18n/locales/pt-BR/common.json b/webui/src/i18n/locales/pt-BR/common.json index ba9ee5681..0f1199824 100644 --- a/webui/src/i18n/locales/pt-BR/common.json +++ b/webui/src/i18n/locales/pt-BR/common.json @@ -791,6 +791,36 @@ "views": "Visualizações de skills", "installedTab": "Instaladas", "discoverTab": "Descobrir", + "customGroup": "Personalizadas", + "builtinGroup": "Integradas", + "otherGroup": "Outras", + "searchInstalled": "Buscar skills instaladas", + "filterAll": "Todas", + "filterEnabled": "Ativadas", + "filterDisabled": "Desativadas", + "noMatching": "Nenhuma skill correspondente.", + "statusDisabled": "Desativada", + "statusEnabled": "Ativada", + "statusNeedsSetup": "Requer configuração", + "showLess": "Mostrar menos", + "showMore": "Mostrar mais", + "enabledControl": "Usar esta skill", + "enabledDescription": "Permite que o agente carregue esta skill quando os requisitos estiverem prontos.", + "enableSkill": "Ativar {{name}}", + "disableSkill": "Desativar {{name}}", + "updateFailed": "Não foi possível atualizar esta skill.", + "deleteTitle": "Excluir skill", + "deleteDescription": "Remove esta skill do workspace atual.", + "deleteAction": "Excluir", + "deleteFailed": "Não foi possível excluir esta skill.", + "deleteConfirmTitle": "Excluir {{name}}?", + "deleteConfirmDescription": "Isso remove os arquivos da skill do workspace atual. Esta ação não pode ser desfeita.", + "deleteConfirmAction": "Excluir skill", + "instructionsTitle": "Instruções da skill", + "setupRequired": "Requer configuração", + "setupDescription": "Instale a dependência ausente na máquina que executa o nanobot e verifique novamente.", + "copySetupCommand": "Copiar comando de configuração", + "checkAgain": "Verificar novamente", "marketplaceSearchFailed": "Não foi possível pesquisar nos mercados de skills.", "marketplaceInstallFailed": "Não foi possível instalar esta skill.", "marketplaceSearchPlaceholder": "Pesquisar skills", diff --git a/webui/src/i18n/locales/vi/common.json b/webui/src/i18n/locales/vi/common.json index a9521eede..1d98ed767 100644 --- a/webui/src/i18n/locales/vi/common.json +++ b/webui/src/i18n/locales/vi/common.json @@ -777,6 +777,36 @@ "views": "Chế độ xem kỹ năng", "installedTab": "Đã cài đặt", "discoverTab": "Khám phá", + "customGroup": "Tùy chỉnh", + "builtinGroup": "Tích hợp sẵn", + "otherGroup": "Khác", + "searchInstalled": "Tìm skill đã cài đặt", + "filterAll": "Tất cả", + "filterEnabled": "Đã bật", + "filterDisabled": "Đã tắt", + "noMatching": "Không có skill phù hợp.", + "statusDisabled": "Đã tắt", + "statusEnabled": "Đã bật", + "statusNeedsSetup": "Cần thiết lập", + "showLess": "Thu gọn", + "showMore": "Hiển thị thêm", + "enabledControl": "Sử dụng skill này", + "enabledDescription": "Cho phép agent tải skill này khi các yêu cầu đã sẵn sàng.", + "enableSkill": "Bật {{name}}", + "disableSkill": "Tắt {{name}}", + "updateFailed": "Không thể cập nhật skill này.", + "deleteTitle": "Xóa skill", + "deleteDescription": "Xóa skill này khỏi workspace hiện tại.", + "deleteAction": "Xóa", + "deleteFailed": "Không thể xóa skill này.", + "deleteConfirmTitle": "Xóa {{name}}?", + "deleteConfirmDescription": "Thao tác này xóa các tệp skill khỏi workspace hiện tại và không thể hoàn tác.", + "deleteConfirmAction": "Xóa skill", + "instructionsTitle": "Hướng dẫn skill", + "setupRequired": "Cần thiết lập", + "setupDescription": "Cài đặt phần phụ thuộc còn thiếu trên máy chạy nanobot rồi kiểm tra lại.", + "copySetupCommand": "Sao chép lệnh thiết lập", + "checkAgain": "Kiểm tra lại", "marketplaceSearchFailed": "Không thể tìm kiếm các kho kỹ năng.", "marketplaceInstallFailed": "Không thể cài đặt kỹ năng này.", "marketplaceSearchPlaceholder": "Tìm kiếm kỹ năng", diff --git a/webui/src/i18n/locales/zh-CN/common.json b/webui/src/i18n/locales/zh-CN/common.json index a0354f2d5..ddd6abc0e 100644 --- a/webui/src/i18n/locales/zh-CN/common.json +++ b/webui/src/i18n/locales/zh-CN/common.json @@ -791,6 +791,36 @@ "views": "技能视图", "installedTab": "已安装", "discoverTab": "发现", + "customGroup": "自定义", + "builtinGroup": "内置", + "otherGroup": "其他", + "searchInstalled": "搜索已安装技能", + "filterAll": "全部", + "filterEnabled": "已启用", + "filterDisabled": "已停用", + "noMatching": "没有匹配的技能。", + "statusDisabled": "已停用", + "statusEnabled": "已启用", + "statusNeedsSetup": "需要设置", + "showLess": "收起", + "showMore": "展开", + "enabledControl": "使用此技能", + "enabledDescription": "当技能需求满足时,允许 agent 加载并使用它。", + "enableSkill": "启用 {{name}}", + "disableSkill": "停用 {{name}}", + "updateFailed": "无法更新此技能。", + "deleteTitle": "删除技能", + "deleteDescription": "从当前工作区移除此技能。", + "deleteAction": "删除", + "deleteFailed": "无法删除此技能。", + "deleteConfirmTitle": "删除 {{name}}?", + "deleteConfirmDescription": "这会从当前工作区移除该技能的文件,且无法撤销。", + "deleteConfirmAction": "删除技能", + "instructionsTitle": "技能说明", + "setupRequired": "需要设置", + "setupDescription": "请在运行 nanobot 的设备上安装缺少的依赖,然后重新检查。", + "copySetupCommand": "复制设置命令", + "checkAgain": "重新检查", "marketplaceSearchFailed": "暂时无法搜索技能市场。", "marketplaceInstallFailed": "无法安装此技能。", "marketplaceSearchPlaceholder": "搜索技能", diff --git a/webui/src/i18n/locales/zh-TW/common.json b/webui/src/i18n/locales/zh-TW/common.json index c4516e645..18cdab1a8 100644 --- a/webui/src/i18n/locales/zh-TW/common.json +++ b/webui/src/i18n/locales/zh-TW/common.json @@ -777,6 +777,36 @@ "views": "技能檢視", "installedTab": "已安裝", "discoverTab": "探索", + "customGroup": "自訂", + "builtinGroup": "內建", + "otherGroup": "其他", + "searchInstalled": "搜尋已安裝技能", + "filterAll": "全部", + "filterEnabled": "已啟用", + "filterDisabled": "已停用", + "noMatching": "沒有相符的技能。", + "statusDisabled": "已停用", + "statusEnabled": "已啟用", + "statusNeedsSetup": "需要設定", + "showLess": "收合", + "showMore": "展開", + "enabledControl": "使用此技能", + "enabledDescription": "當技能需求已滿足時,允許 agent 載入並使用它。", + "enableSkill": "啟用 {{name}}", + "disableSkill": "停用 {{name}}", + "updateFailed": "無法更新此技能。", + "deleteTitle": "刪除技能", + "deleteDescription": "從目前工作區移除此技能。", + "deleteAction": "刪除", + "deleteFailed": "無法刪除此技能。", + "deleteConfirmTitle": "刪除 {{name}}?", + "deleteConfirmDescription": "這會從目前工作區移除該技能的檔案,且無法復原。", + "deleteConfirmAction": "刪除技能", + "instructionsTitle": "技能說明", + "setupRequired": "需要設定", + "setupDescription": "請在執行 nanobot 的裝置上安裝缺少的相依套件,然後重新檢查。", + "copySetupCommand": "複製設定指令", + "checkAgain": "重新檢查", "marketplaceSearchFailed": "暫時無法搜尋技能市集。", "marketplaceInstallFailed": "無法安裝此技能。", "marketplaceSearchPlaceholder": "搜尋技能", diff --git a/webui/src/tests/i18n.test.tsx b/webui/src/tests/i18n.test.tsx index c492dfdfd..e5f3e9d4b 100644 --- a/webui/src/tests/i18n.test.tsx +++ b/webui/src/tests/i18n.test.tsx @@ -78,6 +78,36 @@ const LOCALIZED_SETTINGS_COPY_KEYS = [ "settings.skills.views", "settings.skills.installedTab", "settings.skills.discoverTab", + "settings.skills.customGroup", + "settings.skills.builtinGroup", + "settings.skills.otherGroup", + "settings.skills.searchInstalled", + "settings.skills.filterAll", + "settings.skills.filterEnabled", + "settings.skills.filterDisabled", + "settings.skills.noMatching", + "settings.skills.statusDisabled", + "settings.skills.statusEnabled", + "settings.skills.statusNeedsSetup", + "settings.skills.showLess", + "settings.skills.showMore", + "settings.skills.enabledControl", + "settings.skills.enabledDescription", + "settings.skills.enableSkill", + "settings.skills.disableSkill", + "settings.skills.updateFailed", + "settings.skills.deleteTitle", + "settings.skills.deleteDescription", + "settings.skills.deleteAction", + "settings.skills.deleteFailed", + "settings.skills.deleteConfirmTitle", + "settings.skills.deleteConfirmDescription", + "settings.skills.deleteConfirmAction", + "settings.skills.instructionsTitle", + "settings.skills.setupRequired", + "settings.skills.setupDescription", + "settings.skills.copySetupCommand", + "settings.skills.checkAgain", "settings.skills.marketplaceSearchFailed", "settings.skills.marketplaceInstallFailed", "settings.skills.marketplaceSearchPlaceholder",