fix(batch-install-plugins): support CRLF community plugin metadata

- support CRLF docstrings and folded YAML metadata blocks
- detect community repo plugins such as iChristGit/OpenWebui-Tools correctly
- align README, mirrored docs, and announcement wording with actual behavior

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
fujie
2026-03-15 18:14:23 +08:00
parent cea31fed38
commit c818a2ac8d
8 changed files with 725 additions and 178 deletions

View File

@@ -7,11 +7,13 @@ version: 1.0.0
description: One-click batch install plugins from GitHub repositories to your OpenWebUI instance.
"""
import ast
import asyncio
import json
import logging
import os
import re
import textwrap
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
@@ -33,9 +35,10 @@ SELF_EXCLUDE_TERMS = (
SELF_EXCLUDE_HINT,
"batch install plugins from github",
)
DOCSTRING_PATTERN = re.compile(r'^\s*"""\n(.*?)\n"""', re.DOTALL)
DOCSTRING_PATTERN = re.compile(r'^\s*(?P<quote>"""|\'\'\')\s*(.*?)\s*(?P=quote)', re.DOTALL)
CLASS_PATTERN = re.compile(r'^class (Tools|Filter|Pipe|Action)\s*[\(:]', re.MULTILINE)
EMOJI_PATTERN = re.compile(r'[\U00010000-\U0010ffff]', re.UNICODE)
METADATA_KEY_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_-]*$")
TRANSLATIONS = {
"en-US": {
@@ -476,19 +479,109 @@ class PluginCandidate:
def extract_metadata(content: str) -> Dict[str, str]:
match = DOCSTRING_PATTERN.search(content)
if not match:
docstring = _extract_module_docstring(content)
if not docstring:
return {}
metadata: Dict[str, str] = {}
for raw_line in match.group(1).splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or ":" not in line:
lines = docstring.splitlines()
index = 0
while index < len(lines):
raw_line = lines[index]
stripped = raw_line.strip()
if not stripped or stripped.startswith("#"):
index += 1
continue
key, value = line.split(":", 1)
metadata[key.strip().lower()] = value.strip()
if raw_line[:1].isspace() or ":" not in raw_line:
index += 1
continue
key, value = raw_line.split(":", 1)
key = key.strip().lower()
if not METADATA_KEY_PATTERN.match(key):
index += 1
continue
value = value.strip()
if value and value[0] in {">", "|"}:
block_lines, index = _consume_indented_block(lines, index + 1)
metadata[key] = (
_fold_yaml_block(block_lines)
if value[0] == ">"
else _preserve_yaml_block(block_lines)
)
continue
metadata[key] = value
index += 1
return metadata
def _extract_module_docstring(content: str) -> str:
normalized = content.lstrip("\ufeff")
try:
module = ast.parse(normalized)
except SyntaxError:
module = None
if module is not None:
docstring = ast.get_docstring(module, clean=False)
if isinstance(docstring, str):
return docstring
fallback = normalized.replace("\r\n", "\n").replace("\r", "\n")
match = DOCSTRING_PATTERN.search(fallback)
return match.group(2) if match else ""
def _consume_indented_block(lines: List[str], start_index: int) -> Tuple[List[str], int]:
block: List[str] = []
index = start_index
while index < len(lines):
line = lines[index]
if not line.strip():
block.append("")
index += 1
continue
if line[:1].isspace():
block.append(line)
index += 1
continue
break
dedented = textwrap.dedent("\n".join(block)).splitlines()
return dedented, index
def _fold_yaml_block(lines: List[str]) -> str:
paragraphs: List[str] = []
current: List[str] = []
for line in lines:
stripped = line.strip()
if not stripped:
if current:
paragraphs.append(" ".join(current))
current = []
continue
current.append(stripped)
if current:
paragraphs.append(" ".join(current))
return "\n\n".join(paragraphs).strip()
def _preserve_yaml_block(lines: List[str]) -> str:
return "\n".join(line.rstrip() for line in lines).strip()
def detect_plugin_type(content: str) -> Optional[str]:
if "\nclass Tools:" in content or "\nclass Tools (" in content:
return "tool"
@@ -767,7 +860,7 @@ async def discover_plugins(
skipped.append((item_path, "missing title/description"))
continue
if has_emoji(metadata.get("title", "")):
if is_default_repo and has_emoji(metadata.get("title", "")):
skipped.append((item_path, "title contains emoji"))
continue