fix(batch-install-plugins): handle null exclude_keywords and improve docstrings

- Fix AttributeError when LLM passes null for exclude_keywords parameter (issue #63)
- Improve function docstrings to clarify unified workflow
- Optimize status messages to show repository name
This commit is contained in:
fujie
2026-03-24 18:54:01 +08:00
parent df042e471e
commit b6ce354034

View File

@@ -44,7 +44,7 @@ METADATA_KEY_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_-]*$")
TRANSLATIONS = {
"en-US": {
"status_fetching": "Fetching plugin list from GitHub...",
"status_fetching": "Discovering plugins from {repo}...",
"status_installing": "Installing [{type}] {title}...",
"status_done": "Installation complete: {success}/{total} plugins installed.",
"status_list_title": "Available Plugins ({count} total)",
@@ -67,7 +67,7 @@ TRANSLATIONS = {
"err_no_match": "No plugins match the specified types.",
},
"zh-CN": {
"status_fetching": "正在从 GitHub 获取插件列表...",
"status_fetching": "正在从 {repo} 发现插件...",
"status_installing": "正在安装 [{type}] {title}...",
"status_done": "安装完成:成功安装 {success}/{total} 个插件。",
"status_list_title": "可用插件(共 {count} 个)",
@@ -90,7 +90,7 @@ TRANSLATIONS = {
"err_no_match": "没有符合指定类型的插件。",
},
"zh-HK": {
"status_fetching": "正在從 GitHub 取得外掛列表...",
"status_fetching": "正在從 {repo} 發現外掛...",
"status_installing": "正在安裝 [{type}] {title}...",
"status_done": "安裝完成:成功安裝 {success}/{total} 個外掛。",
"status_list_title": "可用外掛(共 {count} 個)",
@@ -113,7 +113,7 @@ TRANSLATIONS = {
"err_no_match": "沒有符合指定類型的外掛。",
},
"zh-TW": {
"status_fetching": "正在從 GitHub 取得外掛列表...",
"status_fetching": "正在從 {repo} 發現外掛...",
"status_installing": "正在安裝 [{type}] {title}...",
"status_done": "安裝完成:成功安裝 {success}/{total} 個外掛。",
"status_list_title": "可用外掛(共 {count} 個)",
@@ -136,7 +136,7 @@ TRANSLATIONS = {
"err_no_match": "沒有符合指定類型的外掛。",
},
"ko-KR": {
"status_fetching": "GitHub에서 플러그인 목록을 가져오는 중...",
"status_fetching": "{repo}에서 플러그인 검색 중...",
"status_installing": "[{type}] {title} 설치 중...",
"status_done": "설치 완료: {success}/{total}개 플러그인 설치됨.",
"status_list_title": "사용 가능한 플러그인 (총 {count}개)",
@@ -159,7 +159,7 @@ TRANSLATIONS = {
"err_no_match": "지정된 유형과 일치하는 플러그인이 없습니다.",
},
"ja-JP": {
"status_fetching": "GitHubからプラグインリストを取得中...",
"status_fetching": "{repo}からプラグインを検索中...",
"status_installing": "[{type}] {title} をインストール中...",
"status_done": "インストール完了: {success}/{total}個のプラグインがインストールされました。",
"status_list_title": "利用可能なプラグイン (合計{count}個)",
@@ -182,7 +182,7 @@ TRANSLATIONS = {
"err_no_match": "指定されたタイプのプラグインがありません。",
},
"fr-FR": {
"status_fetching": "Récupération de la liste des plugins depuis GitHub...",
"status_fetching": "Recherche de plugins dans {repo}...",
"status_installing": "Installation de [{type}] {title}...",
"status_done": "Installation terminée: {success}/{total} plugins installés.",
"status_list_title": "Plugins disponibles ({count} au total)",
@@ -205,7 +205,7 @@ TRANSLATIONS = {
"err_no_match": "Aucun plugin ne correspond aux types spécifiés.",
},
"de-DE": {
"status_fetching": "Plugin-Liste wird von GitHub abgerufen...",
"status_fetching": "Plugins werden in {repo} gesucht...",
"status_installing": "[{type}] {title} wird installiert...",
"status_done": "Installation abgeschlossen: {success}/{total} Plugins installiert.",
"status_list_title": "Verfügbare Plugins (insgesamt {count})",
@@ -228,7 +228,7 @@ TRANSLATIONS = {
"err_no_match": "Keine Plugins entsprechen den angegebenen Typen.",
},
"es-ES": {
"status_fetching": "Obteniendo lista de plugins de GitHub...",
"status_fetching": "Buscando plugins en {repo}...",
"status_installing": "Instalando [{type}] {title}...",
"status_done": "Instalación completada: {success}/{total} plugins instalados.",
"status_list_title": "Plugins disponibles ({count} en total)",
@@ -251,7 +251,7 @@ TRANSLATIONS = {
"err_no_match": "No hay plugins que coincidan con los tipos especificados.",
},
"it-IT": {
"status_fetching": "Recupero lista plugin da GitHub...",
"status_fetching": "Ricerca plugin in {repo}...",
"status_installing": "Installazione di [{type}] {title}...",
"status_done": "Installazione completata: {success}/{total} plugin installati.",
"status_list_title": "Plugin disponibili ({count} totali)",
@@ -274,7 +274,7 @@ TRANSLATIONS = {
"err_no_match": "Nessun plugin corrisponde ai tipi specificati.",
},
"vi-VN": {
"status_fetching": "Đang lấy danh sách plugin từ GitHub...",
"status_fetching": "Đang tìm kiếm plugin trong {repo}...",
"status_installing": "Đang cài đặt [{type}] {title}...",
"status_done": "Cài đặt hoàn tất: {success}/{total} plugin đã được cài đặt.",
"status_list_title": "Plugin khả dụng ({count} tổng cộng)",
@@ -983,7 +983,7 @@ def _filter_candidates(
if not (c.source_repo.lower() == DEFAULT_REPO.lower() and _matches_self_plugin(c))
]
exclude_list = [item.strip().lower() for item in exclude_keywords.split(",") if item.strip()]
exclude_list = [item.strip().lower() for item in (exclude_keywords or "").split(",") if item.strip()]
if exclude_list:
filtered = [
c
@@ -1458,6 +1458,19 @@ async def discover_plugins(
skip_keywords: str = "test",
source_repo: str = "",
) -> Tuple[List[PluginCandidate], List[Tuple[str, str]]]:
"""Fetch and parse all plugins from a single GitHub repository.
Scans the repo's file tree for Python files, validates each as a plugin,
and extracts metadata (title, description, type, version).
Args:
url: GitHub repository URL (e.g. https://github.com/owner/repo).
skip_keywords: Comma-separated keywords to skip in filenames.
source_repo: Override the repo identifier (owner/repo format).
Returns:
Tuple of (valid_plugins, skipped_files_with_reasons).
"""
parsed = parse_github_url(url)
if not parsed:
return [], [("url", "invalid github url")]
@@ -1539,6 +1552,15 @@ async def discover_plugins_from_repos(
repos: List[str],
skip_keywords: str = "test",
) -> Tuple[List[PluginCandidate], List[Tuple[str, str]]]:
"""Fetch plugins from multiple repositories in parallel.
Args:
repos: List of owner/repo strings (e.g. ["Fu-Jie/openwebui-extensions"]).
skip_keywords: Comma-separated keywords to skip in filenames.
Returns:
Tuple of (all_plugins, all_skipped_files_with_reasons).
"""
tasks = [
discover_plugins(f"https://github.com/{repo}", skip_keywords, source_repo=repo)
for repo in repos
@@ -1604,10 +1626,18 @@ class Tools:
repo: str = DEFAULT_REPO,
plugin_types: List[str] = ["pipe", "action", "filter", "tool"],
) -> str:
"""List plugins from one or more repositories in a single call.
"""List all available plugins without installing.
If a user request mentions multiple repositories, combine them into the
`repo` argument instead of calling this tool multiple times.
Use this to preview what plugins are available before installation.
For installation, use install_all_plugins instead.
Args:
repo: One or more GitHub repositories (owner/repo format), comma or
semicolon separated. Defaults to Fu-Jie/openwebui-extensions.
plugin_types: Filter by plugin type (pipe, action, filter, tool).
Returns:
Markdown formatted list of available plugins grouped by repository.
"""
user_ctx = await _get_user_context(__user__, __event_call__, __request__)
lang = user_ctx.get("user_language", "en-US")
@@ -1650,11 +1680,25 @@ class Tools:
exclude_keywords: str = "",
timeout: int = DEFAULT_TIMEOUT,
) -> str:
"""Install plugins from one or more repositories in a single call.
"""Discover and install plugins interactively.
If a user request mentions multiple repositories, combine them into the
`repo` argument and call this tool once instead of making parallel
calls for each repository.
Always fetches all available plugins regardless of user input, then
presents a selection dialog for the user to choose which to install.
Workflow:
1. Discover all plugins from repo(s)
2. Present interactive selection dialog
3. Install selected plugins to OpenWebUI
Args:
repo: One or more GitHub repositories (owner/repo format), comma or
semicolon separated. Defaults to Fu-Jie/openwebui-extensions.
plugin_types: Filter by plugin type (pipe, action, filter, tool).
exclude_keywords: Comma-separated keywords to skip matching plugins.
timeout: HTTP request timeout in seconds.
Returns:
Status message with installation results.
"""
user_ctx = await _get_user_context(__user__, __event_call__, __request__)
lang = user_ctx.get("user_language", "en-US")
@@ -1705,9 +1749,10 @@ class Tools:
base_url = base_url.rstrip("/")
await _emit_status(event_emitter, _t(lang, "status_fetching"), done=False)
repo_list = _parse_repo_inputs(repo)
repo_display = repo_list[0] if len(repo_list) == 1 else f"{repo_list[0]} +{len(repo_list)-1}"
await _emit_status(event_emitter, _t(lang, "status_fetching", repo=repo_display), done=False)
candidates, _ = await discover_plugins_from_repos(repo_list, skip_keywords)
if not candidates: