From eb79bc9633940cae00139aab99e91db51afa6827 Mon Sep 17 00:00:00 2001 From: fujie Date: Sat, 28 Feb 2026 23:06:08 +0800 Subject: [PATCH] fix(tools): release openwebui-skills-manager v0.2.1 - Add GitHub skills-directory auto-discovery for install_skill batch install from tree root URLs. - Harden language detection with frontend-first fallback (__event_call__ + timeout), then headers/profile. - Bump plugin/docs versions to 0.2.1 and sync bilingual README/docs/index entries. --- docs/plugins/tools/index.md | 2 +- docs/plugins/tools/index.zh.md | 2 +- .../tools/openwebui-skills-manager-tool.md | 7 +- .../tools/openwebui-skills-manager-tool.zh.md | 7 +- .../tools/openwebui-skills-manager/README.md | 7 +- .../openwebui-skills-manager/README_CN.md | 7 +- .../openwebui_skills_manager.py | 216 ++++++++++++++++-- 7 files changed, 221 insertions(+), 27 deletions(-) diff --git a/docs/plugins/tools/index.md b/docs/plugins/tools/index.md index 09eb39b..2e01e47 100644 --- a/docs/plugins/tools/index.md +++ b/docs/plugins/tools/index.md @@ -4,4 +4,4 @@ OpenWebUI native Tool plugins that can be used across models. ## Available Tool Plugins -- [OpenWebUI Skills Manager Tool](openwebui-skills-manager-tool.md) (v0.2.0) - Simple native skill management (`list/show/install/create/update/delete`). +- [OpenWebUI Skills Manager Tool](openwebui-skills-manager-tool.md) (v0.2.1) - Simple native skill management (`list/show/install/create/update/delete`). diff --git a/docs/plugins/tools/index.zh.md b/docs/plugins/tools/index.zh.md index c057e5b..9a35cd8 100644 --- a/docs/plugins/tools/index.zh.md +++ b/docs/plugins/tools/index.zh.md @@ -4,4 +4,4 @@ ## 可用 Tool 插件 -- [OpenWebUI Skills 管理工具](openwebui-skills-manager-tool.zh.md) (v0.2.0) - 简化技能管理(`list/show/install/create/update/delete`)。 +- [OpenWebUI Skills 管理工具](openwebui-skills-manager-tool.zh.md) (v0.2.1) - 简化技能管理(`list/show/install/create/update/delete`)。 diff --git a/docs/plugins/tools/openwebui-skills-manager-tool.md b/docs/plugins/tools/openwebui-skills-manager-tool.md index 037c5bf..17445a8 100644 --- a/docs/plugins/tools/openwebui-skills-manager-tool.md +++ b/docs/plugins/tools/openwebui-skills-manager-tool.md @@ -1,9 +1,14 @@ # OpenWebUI Skills Manager Tool -**Author:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **Version:** 0.2.0 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) +**Author:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **Version:** 0.2.1 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) A standalone OpenWebUI Tool plugin for managing native Workspace Skills across models. +## What's New + +- Added GitHub skills-directory auto-discovery for `install_skill` (e.g., `.../tree/main/skills`) to install all child skills in one request. +- Fixed language detection with robust frontend-first fallback (`__event_call__` + timeout), request header fallback, and profile fallback. + ## Key Features - Native skill management diff --git a/docs/plugins/tools/openwebui-skills-manager-tool.zh.md b/docs/plugins/tools/openwebui-skills-manager-tool.zh.md index 3c7f43a..b69bdc4 100644 --- a/docs/plugins/tools/openwebui-skills-manager-tool.zh.md +++ b/docs/plugins/tools/openwebui-skills-manager-tool.zh.md @@ -1,9 +1,14 @@ # OpenWebUI Skills 管理工具 -**Author:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **Version:** 0.2.0 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) +**Author:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **Version:** 0.2.1 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) 一个可跨模型使用的 OpenWebUI 原生 Tool 插件,用于管理 Workspace Skills。 +## 最新更新 + +- `install_skill` 新增 GitHub 技能目录自动发现(例如 `.../tree/main/skills`),可一键安装目录下所有子技能。 +- 修复语言获取逻辑:前端优先(`__event_call__` + 超时保护),并回退到请求头与用户资料。 + ## 核心特性 - 原生技能管理 diff --git a/plugins/tools/openwebui-skills-manager/README.md b/plugins/tools/openwebui-skills-manager/README.md index 767db8b..f9065a3 100644 --- a/plugins/tools/openwebui-skills-manager/README.md +++ b/plugins/tools/openwebui-skills-manager/README.md @@ -1,9 +1,14 @@ # 🧰 OpenWebUI Skills Manager Tool -**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.2.0 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) +**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.2.1 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) A standalone OpenWebUI Tool plugin to manage native **Workspace > Skills** for any model. +## What's New + +- Added GitHub skills-directory auto-discovery for `install_skill` (e.g., `.../tree/main/skills`) to install all child skills in one request. +- Fixed language detection with robust frontend-first fallback (`__event_call__` + timeout), request header fallback, and profile fallback. + ## Key Features - **🌐 Model-agnostic**: Can be enabled for any model that supports OpenWebUI Tools. diff --git a/plugins/tools/openwebui-skills-manager/README_CN.md b/plugins/tools/openwebui-skills-manager/README_CN.md index 0a6bcfc..08a130e 100644 --- a/plugins/tools/openwebui-skills-manager/README_CN.md +++ b/plugins/tools/openwebui-skills-manager/README_CN.md @@ -1,9 +1,14 @@ # 🧰 OpenWebUI Skills 管理工具 -**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.2.0 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) +**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.2.1 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) 一个 OpenWebUI 原生 Tool 插件,用于让任意模型直接管理 **Workspace > Skills**。 +## 最新更新 + +- `install_skill` 新增 GitHub 技能目录自动发现(例如 `.../tree/main/skills`),可一键安装目录下所有子技能。 +- 修复语言获取逻辑:前端优先(`__event_call__` + 超时保护),并回退到请求头与用户资料。 + ## 核心特性 - **🌐 全模型可用**:只要模型启用了 OpenWebUI Tools,即可调用。 diff --git a/plugins/tools/openwebui-skills-manager/openwebui_skills_manager.py b/plugins/tools/openwebui-skills-manager/openwebui_skills_manager.py index eef5ed2..7fac06c 100644 --- a/plugins/tools/openwebui-skills-manager/openwebui_skills_manager.py +++ b/plugins/tools/openwebui-skills-manager/openwebui_skills_manager.py @@ -3,13 +3,14 @@ title: OpenWebUI Skills Manager Tool author: Fu-Jie author_url: https://github.com/Fu-Jie/openwebui-extensions funding_url: https://github.com/open-webui -version: 0.2.0 +version: 0.2.1 openwebui_id: b4bce8e4-08e7-4f90-bea7-dc31d463a0bb requirements: description: Standalone OpenWebUI tool for managing native Workspace Skills (list/show/install/create/update/delete) for any model. """ import asyncio +import json import logging import re import tempfile @@ -36,6 +37,8 @@ BASE_TRANSLATIONS = { "status_listing": "Listing your skills...", "status_showing": "Reading skill details...", "status_installing": "Installing skill from URL...", + "status_installing_batch": "Installing {total} skill(s)...", + "status_discovering_skills": "Discovering skills in {url}...", "status_creating": "Creating skill...", "status_updating": "Updating skill...", "status_deleting": "Deleting skill...", @@ -48,6 +51,7 @@ BASE_TRANSLATIONS = { "status_create_overwrite_done": "Updated existing skill: {name}.", "status_update_done": "Updated skill: {name}.", "status_delete_done": "Deleted skill: {name}.", + "status_install_batch_done": "Batch install completed: {succeeded} succeeded, {failed} failed.", "err_unavailable": "OpenWebUI Skills model is unavailable in this runtime.", "err_user_required": "User context is required.", "err_name_required": "Skill name is required.", @@ -69,6 +73,8 @@ TRANSLATIONS = { "status_listing": "正在列出你的技能...", "status_showing": "正在读取技能详情...", "status_installing": "正在从 URL 安装技能...", + "status_installing_batch": "正在安装 {total} 个技能...", + "status_discovering_skills": "正在从 {url} 发现技能...", "status_creating": "正在创建技能...", "status_updating": "正在更新技能...", "status_deleting": "正在删除技能...", @@ -81,6 +87,7 @@ TRANSLATIONS = { "status_create_overwrite_done": "已更新同名技能:{name}。", "status_update_done": "技能更新完成:{name}。", "status_delete_done": "技能删除完成:{name}。", + "status_install_batch_done": "批量安装完成:成功 {succeeded} 个,失败 {failed} 个。", "err_unavailable": "当前运行环境不可用 OpenWebUI Skills 模型。", "err_user_required": "需要用户上下文。", "err_name_required": "技能名称不能为空。", @@ -99,6 +106,7 @@ TRANSLATIONS = { "status_listing": "正在列出你的技能...", "status_showing": "正在讀取技能詳情...", "status_installing": "正在從 URL 安裝技能...", + "status_installing_batch": "正在安裝 {total} 個技能...", "status_creating": "正在建立技能...", "status_updating": "正在更新技能...", "status_deleting": "正在刪除技能...", @@ -111,6 +119,7 @@ TRANSLATIONS = { "status_create_overwrite_done": "已更新同名技能:{name}。", "status_update_done": "技能更新完成:{name}。", "status_delete_done": "技能刪除完成:{name}。", + "status_install_batch_done": "批次安裝完成:成功 {succeeded} 個,失敗 {failed} 個。", "err_unavailable": "目前執行環境不可用 OpenWebUI Skills 模型。", "err_user_required": "需要使用者上下文。", "err_name_required": "技能名稱不能為空。", @@ -129,6 +138,7 @@ TRANSLATIONS = { "status_listing": "正在列出你的技能...", "status_showing": "正在讀取技能詳情...", "status_installing": "正在從 URL 安裝技能...", + "status_installing_batch": "正在安裝 {total} 個技能...", "status_creating": "正在建立技能...", "status_updating": "正在更新技能...", "status_deleting": "正在刪除技能...", @@ -141,6 +151,7 @@ TRANSLATIONS = { "status_create_overwrite_done": "已更新同名技能:{name}。", "status_update_done": "技能更新完成:{name}。", "status_delete_done": "技能刪除完成:{name}。", + "status_install_batch_done": "批次安裝完成:成功 {succeeded} 個,失敗 {failed} 個。", "err_unavailable": "目前執行環境不可用 OpenWebUI Skills 模型。", "err_user_required": "需要使用者上下文。", "err_name_required": "技能名稱不能為空。", @@ -159,6 +170,8 @@ TRANSLATIONS = { "status_listing": "スキル一覧を取得しています...", "status_showing": "スキル詳細を読み込み中...", "status_installing": "URL からスキルをインストール中...", + "status_installing_batch": "{total} 件のスキルをインストール中...", + "status_discovering_skills": "{url} からスキルを検出中...", "status_creating": "スキルを作成中...", "status_updating": "スキルを更新中...", "status_deleting": "スキルを削除中...", @@ -171,6 +184,7 @@ TRANSLATIONS = { "status_create_overwrite_done": "同名スキルを更新しました: {name}。", "status_update_done": "スキルを更新しました: {name}。", "status_delete_done": "スキルを削除しました: {name}。", + "status_install_batch_done": "一括インストール完了: 成功 {succeeded} 件、失敗 {failed} 件。", "err_unavailable": "この実行環境では OpenWebUI Skills モデルを利用できません。", "err_user_required": "ユーザーコンテキストが必要です。", "err_name_required": "スキル名は必須です。", @@ -189,6 +203,8 @@ TRANSLATIONS = { "status_listing": "스킬 목록을 불러오는 중...", "status_showing": "스킬 상세 정보를 읽는 중...", "status_installing": "URL에서 스킬 설치 중...", + "status_installing_batch": "스킬 {total}개를 설치하는 중...", + "status_discovering_skills": "{url}에서 스킬 발견 중...", "status_creating": "스킬 생성 중...", "status_updating": "스킬 업데이트 중...", "status_deleting": "스킬 삭제 중...", @@ -201,6 +217,7 @@ TRANSLATIONS = { "status_create_overwrite_done": "동일 이름 스킬 업데이트 완료: {name}.", "status_update_done": "스킬 업데이트 완료: {name}.", "status_delete_done": "스킬 삭제 완료: {name}.", + "status_install_batch_done": "일괄 설치 완료: 성공 {succeeded}개, 실패 {failed}개.", "err_unavailable": "현재 런타임에서 OpenWebUI Skills 모델을 사용할 수 없습니다.", "err_user_required": "사용자 컨텍스트가 필요합니다.", "err_name_required": "스킬 이름은 필수입니다.", @@ -219,6 +236,8 @@ TRANSLATIONS = { "status_listing": "Liste des skills en cours...", "status_showing": "Lecture des détails du skill...", "status_installing": "Installation du skill depuis l'URL...", + "status_installing_batch": "Installation de {total} skill(s)...", + "status_discovering_skills": "Découverte de skills dans {url}...", "status_creating": "Création du skill...", "status_updating": "Mise à jour du skill...", "status_deleting": "Suppression du skill...", @@ -231,6 +250,7 @@ TRANSLATIONS = { "status_create_overwrite_done": "Skill existant mis à jour : {name}.", "status_update_done": "Skill mis à jour : {name}.", "status_delete_done": "Skill supprimé : {name}.", + "status_install_batch_done": "Installation en lot terminée : {succeeded} réussies, {failed} échouées.", "err_unavailable": "Le modèle OpenWebUI Skills n'est pas disponible dans cet environnement.", "err_user_required": "Le contexte utilisateur est requis.", "err_name_required": "Le nom du skill est requis.", @@ -249,6 +269,8 @@ TRANSLATIONS = { "status_listing": "Deine Skills werden aufgelistet...", "status_showing": "Skill-Details werden gelesen...", "status_installing": "Skill wird von URL installiert...", + "status_installing_batch": "{total} Skill(s) werden installiert...", + "status_discovering_skills": "Suche nach Skills in {url}...", "status_creating": "Skill wird erstellt...", "status_updating": "Skill wird aktualisiert...", "status_deleting": "Skill wird gelöscht...", @@ -261,6 +283,7 @@ TRANSLATIONS = { "status_create_overwrite_done": "Bestehender Skill aktualisiert: {name}.", "status_update_done": "Skill aktualisiert: {name}.", "status_delete_done": "Skill gelöscht: {name}.", + "status_install_batch_done": "Batch-Installation abgeschlossen: {succeeded} erfolgreich, {failed} fehlgeschlagen.", "err_unavailable": "Das OpenWebUI-Skills-Modell ist in dieser Laufzeit nicht verfügbar.", "err_user_required": "Benutzerkontext ist erforderlich.", "err_name_required": "Skill-Name ist erforderlich.", @@ -279,6 +302,8 @@ TRANSLATIONS = { "status_listing": "Listando tus skills...", "status_showing": "Leyendo detalles del skill...", "status_installing": "Instalando skill desde URL...", + "status_installing_batch": "Instalando {total} skill(s)...", + "status_discovering_skills": "Descubriendo skills en {url}...", "status_creating": "Creando skill...", "status_updating": "Actualizando skill...", "status_deleting": "Eliminando skill...", @@ -291,6 +316,7 @@ TRANSLATIONS = { "status_create_overwrite_done": "Skill existente actualizado: {name}.", "status_update_done": "Skill actualizado: {name}.", "status_delete_done": "Skill eliminado: {name}.", + "status_install_batch_done": "Instalación por lotes completada: {succeeded} correctas, {failed} fallidas.", "err_unavailable": "El modelo OpenWebUI Skills no está disponible en este entorno.", "err_user_required": "Se requiere contexto de usuario.", "err_name_required": "Se requiere el nombre del skill.", @@ -309,6 +335,8 @@ TRANSLATIONS = { "status_listing": "Elenco delle skill in corso...", "status_showing": "Lettura dei dettagli della skill...", "status_installing": "Installazione della skill da URL...", + "status_installing_batch": "Installazione di {total} skill in corso...", + "status_discovering_skills": "Scoperta di skills in {url}...", "status_creating": "Creazione della skill...", "status_updating": "Aggiornamento della skill...", "status_deleting": "Eliminazione della skill...", @@ -321,6 +349,7 @@ TRANSLATIONS = { "status_create_overwrite_done": "Skill esistente aggiornata: {name}.", "status_update_done": "Skill aggiornata: {name}.", "status_delete_done": "Skill eliminata: {name}.", + "status_install_batch_done": "Installazione batch completata: {succeeded} riuscite, {failed} non riuscite.", "err_unavailable": "Il modello OpenWebUI Skills non è disponibile in questo runtime.", "err_user_required": "È richiesto il contesto utente.", "err_name_required": "Il nome della skill è obbligatorio.", @@ -339,6 +368,8 @@ TRANSLATIONS = { "status_listing": "Đang liệt kê kỹ năng của bạn...", "status_showing": "Đang đọc chi tiết kỹ năng...", "status_installing": "Đang cài đặt kỹ năng từ URL...", + "status_installing_batch": "Đang cài đặt {total} kỹ năng...", + "status_discovering_skills": "Đang phát hiện kỹ năng trong {url}...", "status_creating": "Đang tạo kỹ năng...", "status_updating": "Đang cập nhật kỹ năng...", "status_deleting": "Đang xóa kỹ năng...", @@ -351,6 +382,7 @@ TRANSLATIONS = { "status_create_overwrite_done": "Đã cập nhật kỹ năng cùng tên: {name}.", "status_update_done": "Cập nhật kỹ năng hoàn tất: {name}.", "status_delete_done": "Xóa kỹ năng hoàn tất: {name}.", + "status_install_batch_done": "Cài đặt hàng loạt hoàn tất: thành công {succeeded}, thất bại {failed}.", "err_unavailable": "Mô hình OpenWebUI Skills không khả dụng trong môi trường hiện tại.", "err_user_required": "Cần có ngữ cảnh người dùng.", "err_name_required": "Tên kỹ năng là bắt buộc.", @@ -369,6 +401,8 @@ TRANSLATIONS = { "status_listing": "Sedang menampilkan daftar skill Anda...", "status_showing": "Sedang membaca detail skill...", "status_installing": "Sedang memasang skill dari URL...", + "status_installing_batch": "Sedang memasang {total} skill...", + "status_discovering_skills": "Sedang mencari skill di {url}...", "status_creating": "Sedang membuat skill...", "status_updating": "Sedang memperbarui skill...", "status_deleting": "Sedang menghapus skill...", @@ -381,6 +415,7 @@ TRANSLATIONS = { "status_create_overwrite_done": "Skill dengan nama sama berhasil diperbarui: {name}.", "status_update_done": "Skill berhasil diperbarui: {name}.", "status_delete_done": "Skill berhasil dihapus: {name}.", + "status_install_batch_done": "Pemasangan batch selesai: {succeeded} berhasil, {failed} gagal.", "err_unavailable": "Model OpenWebUI Skills tidak tersedia di runtime ini.", "err_user_required": "Konteks pengguna diperlukan.", "err_name_required": "Nama skill wajib diisi.", @@ -438,14 +473,28 @@ class Tools: def _resolve_language(self, user_language: str) -> str: """Normalize user language code to a supported translation key.""" - if not user_language: + value = str(user_language or "").strip() + if not value: return "en-US" - if user_language in TRANSLATIONS: - return user_language - if user_language in FALLBACK_MAP: - return FALLBACK_MAP[user_language] - base = user_language.split("-")[0] - return FALLBACK_MAP.get(base, "en-US") + + normalized = value.replace("_", "-") + + if normalized in TRANSLATIONS: + return normalized + + lower_to_lang = {k.lower(): k for k in TRANSLATIONS.keys()} + if normalized.lower() in lower_to_lang: + return lower_to_lang[normalized.lower()] + + if normalized in FALLBACK_MAP: + return FALLBACK_MAP[normalized] + + lower_fallback = {k.lower(): v for k, v in FALLBACK_MAP.items()} + if normalized.lower() in lower_fallback: + return lower_fallback[normalized.lower()] + + base = normalized.split("-")[0].lower() + return lower_fallback.get(base, "en-US") def _t(self, lang: str, key: str, **kwargs) -> str: """Return translated text for key with safe formatting.""" @@ -460,8 +509,13 @@ class Tools: pass return text - def _get_user_context(self, __user__: Optional[dict]) -> Dict[str, str]: - """Extract robust user context from OpenWebUI's __user__ payload.""" + async def _get_user_context( + self, + __user__: Optional[dict], + __event_call__: Optional[Any] = None, + __request__: Optional[Any] = None, + ) -> Dict[str, str]: + """Extract robust user context with frontend language fallback.""" if isinstance(__user__, (list, tuple)): user_data = __user__[0] if __user__ else {} elif isinstance(__user__, dict): @@ -469,10 +523,41 @@ class Tools: else: user_data = {} + user_language = user_data.get("language", "en-US") + + if __request__ and hasattr(__request__, "headers"): + accept_lang = __request__.headers.get("accept-language", "") + if accept_lang: + user_language = accept_lang.split(",")[0].split(";")[0] + + if __event_call__: + try: + js_code = """ + try { + return ( + document.documentElement.lang || + localStorage.getItem('locale') || + localStorage.getItem('language') || + navigator.language || + 'en-US' + ); + } catch (e) { + return 'en-US'; + } + """ + frontend_lang = await asyncio.wait_for( + __event_call__({"type": "execute", "data": {"code": js_code}}), + timeout=2.0, + ) + if frontend_lang and isinstance(frontend_lang, str): + user_language = frontend_lang + except Exception as e: + logger.warning(f"Failed to retrieve frontend language: {e}") + return { "user_id": str(user_data.get("id", "")).strip(), "user_name": user_data.get("name", "User"), - "user_language": user_data.get("language", "en-US"), + "user_language": user_language, } async def _emit_status( @@ -543,6 +628,49 @@ class Tools: pass return "" + async def _discover_skills_from_github_directory( + self, url: str, lang: str + ) -> List[str]: + """ + Discover all skill subdirectories from a GitHub tree URL. + Uses GitHub API to list directory contents. + + Example: https://github.com/anthropics/skills/tree/main/skills + Returns: List of individual skill tree URLs for each subdirectory + """ + skill_urls = [] + match = re.match( + r"https://github\.com/([^/]+)/([^/]+)/tree/([^/]+)(/.*)?\Z", url + ) + if not match: + return skill_urls + + owner = match.group(1) + repo = match.group(2) + branch = match.group(3) + path = match.group(4) or "" + + try: + api_url = f"https://api.github.com/repos/{owner}/{repo}/contents{path}?ref={branch}" + response_bytes = await self._fetch_bytes(api_url) + contents = json.loads(response_bytes.decode("utf-8")) + + if isinstance(contents, list): + for item in contents: + if item.get("type") == "dir": + subdir_name = item.get("name", "") + if subdir_name and not subdir_name.startswith("."): + subdir_url = f"https://github.com/{owner}/{repo}/tree/{branch}{path}/{subdir_name}" + skill_urls.append(subdir_url) + + skill_urls.sort() + except Exception as e: + logger.warning( + f"Failed to discover skills from GitHub directory {url}: {e}" + ) + + return skill_urls + def _resolve_github_tree_urls(self, url: str) -> List[str]: """For GitHub tree URLs, resolve to direct file URLs to try. @@ -665,9 +793,11 @@ class Tools: include_content: bool = False, __user__: Optional[dict] = None, __event_emitter__: Optional[Any] = None, + __event_call__: Optional[Any] = None, + __request__: Optional[Any] = None, ) -> Dict[str, Any]: """List current user's OpenWebUI skills.""" - user_ctx = self._get_user_context(__user__) + user_ctx = await self._get_user_context(__user__, __event_call__, __request__) lang = user_ctx["user_language"] user_id = user_ctx["user_id"] @@ -722,9 +852,11 @@ class Tools: include_content: bool = True, __user__: Optional[dict] = None, __event_emitter__: Optional[Any] = None, + __event_call__: Optional[Any] = None, + __request__: Optional[Any] = None, ) -> Dict[str, Any]: """Show one skill by id or name.""" - user_ctx = self._get_user_context(__user__) + user_ctx = await self._get_user_context(__user__, __event_call__, __request__) lang = user_ctx["user_language"] user_id = user_ctx["user_id"] @@ -922,25 +1054,35 @@ class Tools: overwrite: bool = True, __user__: Optional[dict] = None, __event_emitter__: Optional[Any] = None, + __event_call__: Optional[Any] = None, + __request__: Optional[Any] = None, ) -> Dict[str, Any]: - """Install one or more skills from URL(s). Overwrites existing skills by default. + """Install one or more skills from URL(s), with support for GitHub directory auto-discovery. Args: url: A single URL string OR a JSON array of URL strings for batch install. Examples: Single: "https://github.com/owner/repo/tree/main/skills/xlsx" + Directory: "https://github.com/owner/repo/tree/main/skills" Batch: ["https://github.com/owner/repo/tree/main/skills/xlsx", "https://github.com/owner/repo/tree/main/skills/csv"] name: Optional custom name for the skill (single install only). overwrite: If True (default), overwrites any existing skill with the same name. + Auto-Discovery Feature: + If a GitHub tree URL points to a directory that contains multiple skill subdirectories, + this tool will automatically discover all subdirectories and batch install them. + Example: "https://github.com/anthropics/skills/tree/main/skills" will auto-discover + all skill folders under /skills and install them all at once. + Supported URL formats: - GitHub tree URL: https://github.com/owner/repo/tree/branch/path/to/skill + - GitHub skill directory (auto-discovery): https://github.com/owner/repo/tree/branch/path - GitHub blob URL: https://github.com/owner/repo/blob/branch/path/SKILL.md - Raw markdown URL: https://raw.githubusercontent.com/.../SKILL.md - Archive URL: https://example.com/skill.zip (must contain SKILL.md or README.md) """ - user_ctx = self._get_user_context(__user__) + user_ctx = await self._get_user_context(__user__, __event_call__, __request__) lang = user_ctx["user_language"] user_id = user_ctx["user_id"] @@ -949,14 +1091,35 @@ class Tools: if not user_id: raise ValueError(self._t(lang, "err_user_required")) - # Check if url is a list/tuple (batch mode) + # Stage 1: Check for directory auto-discovery (single string GitHub URL) + if isinstance(url, str) and "github.com" in url and "/tree/" in url: + await self._emit_status( + __event_emitter__, + self._t(lang, "status_discovering_skills", url=(url or "")[-50:]), + ) + discover_fn = getattr( + self, "_discover_skills_from_github_directory", None + ) + discovered = [] + if callable(discover_fn): + discovered = await discover_fn(url, lang) + else: + logger.warning( + "_discover_skills_from_github_directory is unavailable on current Tools instance." + ) + if discovered: + # Auto-discovered subdirectories, treat as batch + url = discovered + + # Stage 2: Check if url is a list/tuple (batch mode) if isinstance(url, (list, tuple)): urls = url if not urls: raise ValueError(self._t(lang, "err_url_required")) await self._emit_status( - __event_emitter__, f"Installing {len(urls)} skill(s)..." + __event_emitter__, + self._t(lang, "status_installing_batch", total=len(urls)), ) results = [] @@ -977,7 +1140,12 @@ class Tools: await self._emit_status( __event_emitter__, - f"Batch install completed: {success_count} succeeded, {error_count} failed.", + self._t( + lang, + "status_install_batch_done", + succeeded=success_count, + failed=error_count, + ), done=True, ) @@ -1036,9 +1204,11 @@ class Tools: overwrite: bool = False, __user__: Optional[dict] = None, __event_emitter__: Optional[Any] = None, + __event_call__: Optional[Any] = None, + __request__: Optional[Any] = None, ) -> Dict[str, Any]: """Create a new skill, or update same-name skill when overwrite is enabled.""" - user_ctx = self._get_user_context(__user__) + user_ctx = await self._get_user_context(__user__, __event_call__, __request__) lang = user_ctx["user_language"] user_id = user_ctx["user_id"] @@ -1131,9 +1301,11 @@ class Tools: is_active: Optional[bool] = None, __user__: Optional[dict] = None, __event_emitter__: Optional[Any] = None, + __event_call__: Optional[Any] = None, + __request__: Optional[Any] = None, ) -> Dict[str, Any]: """Update one skill's fields by id or name.""" - user_ctx = self._get_user_context(__user__) + user_ctx = await self._get_user_context(__user__, __event_call__, __request__) lang = user_ctx["user_language"] user_id = user_ctx["user_id"] @@ -1200,9 +1372,11 @@ class Tools: name: str = "", __user__: Optional[dict] = None, __event_emitter__: Optional[Any] = None, + __event_call__: Optional[Any] = None, + __request__: Optional[Any] = None, ) -> Dict[str, Any]: """Delete one skill by id or name.""" - user_ctx = self._get_user_context(__user__) + user_ctx = await self._get_user_context(__user__, __event_call__, __request__) lang = user_ctx["user_language"] user_id = user_ctx["user_id"]