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.
This commit is contained in:
fujie
2026-02-28 23:06:08 +08:00
parent 0c7d427b93
commit eb79bc9633
7 changed files with 221 additions and 27 deletions

View File

@@ -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`).

View File

@@ -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`)。

View File

@@ -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

View File

@@ -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__` + 超时保护),并回退到请求头与用户资料。
## 核心特性
- 原生技能管理

View File

@@ -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.

View File

@@ -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即可调用。

View File

@@ -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"]