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:
@@ -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.
|
||||
|
||||
@@ -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,即可调用。
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user