#!/usr/bin/env python3 """ Script to check and enforce version consistency across OpenWebUI plugins and documentation. 用于检查并强制 OpenWebUI 插件和文档之间版本一致性的脚本。 Usage: python scripts/check_version_consistency.py # Check only python scripts/check_version_consistency.py --fix # Check and fix """ import argparse import os import re import sys from pathlib import Path from typing import Optional, List, Dict, Tuple # ANSI colors GREEN = "\033[92m" RED = "\033[91m" YELLOW = "\033[93m" BLUE = "\033[94m" RESET = "\033[0m" def log_info(msg): print(f"{BLUE}[INFO]{RESET} {msg}") def log_success(msg): print(f"{GREEN}[OK]{RESET} {msg}") def log_warning(msg): print(f"{YELLOW}[WARN]{RESET} {msg}") def log_error(msg): print(f"{RED}[ERR]{RESET} {msg}") class VersionChecker: def __init__(self, root_dir: str, fix: bool = False): self.root_dir = Path(root_dir) self.plugins_dir = self.root_dir / "plugins" self.docs_dir = self.root_dir / "docs" / "plugins" self.fix = fix self.issues_found = 0 self.fixed_count = 0 def extract_version_from_py(self, file_path: Path) -> Optional[str]: """Extract version from Python docstring.""" try: content = file_path.read_text(encoding="utf-8") match = re.search(r"version:\s*([\d\.]+)", content) if match: return match.group(1) except Exception as e: log_error(f"Failed to read {file_path}: {e}") return None def update_file_content( self, file_path: Path, pattern: str, replacement: str, version: str ) -> bool: """Update file content with new version.""" try: content = file_path.read_text(encoding="utf-8") new_content = re.sub(pattern, replacement, content) if content != new_content: if self.fix: file_path.write_text(new_content, encoding="utf-8") log_success( f"Fixed {file_path.relative_to(self.root_dir)}: -> {version}" ) self.fixed_count += 1 return True else: log_error( f"Mismatch in {file_path.relative_to(self.root_dir)}: Expected {version}" ) self.issues_found += 1 return False return True except Exception as e: log_error(f"Failed to update {file_path}: {e}") return False def check_plugin(self, plugin_type: str, plugin_dir: Path): """Check consistency for a single plugin.""" plugin_name = plugin_dir.name # 1. Identify Source of Truth (English .py file) py_file = plugin_dir / f"{plugin_name}.py" if not py_file.exists(): # Try finding any .py file that matches the directory name pattern or is the main file py_files = list(plugin_dir.glob("*.py")) # Filter out _cn.py, templates, etc. candidates = [ f for f in py_files if not f.name.endswith("_cn.py") and "TEMPLATE" not in f.name ] if candidates: py_file = candidates[0] else: return # Not a valid plugin dir true_version = self.extract_version_from_py(py_file) if not true_version: log_warning(f"Skipping {plugin_name}: No version found in {py_file.name}") return log_info(f"Checking {plugin_name} (v{true_version})...") # 2. Check Chinese .py file cn_py_files = list(plugin_dir.glob("*_cn.py")) + list( plugin_dir.glob("*中文*.py") ) # Also check for files that are not the main file but might be the CN version for f in plugin_dir.glob("*.py"): if f != py_file and "TEMPLATE" not in f.name and f not in cn_py_files: # Heuristic: if it has Chinese characters or ends in _cn if re.search(r"[\u4e00-\u9fff]", f.name) or f.name.endswith("_cn.py"): cn_py_files.append(f) for cn_py in set(cn_py_files): self.update_file_content( cn_py, r"(version:\s*)([\d\.]+)", rf"\g<1>{true_version}", true_version ) # 3. Check README.md (English) readme = plugin_dir / "README.md" if readme.exists(): # Pattern 1: **Version:** 1.0.0 self.update_file_content( readme, r"(\*\*Version:?\*\*\s*)([\d\.]+)", rf"\g<1>{true_version}", true_version, ) # Pattern 2: | **Version:** 1.0.0 | self.update_file_content( readme, r"(\|\s*\*\*Version:\*\*\s*)([\d\.]+)", rf"\g<1>{true_version}", true_version, ) # 4. Check README_CN.md (Chinese) readme_cn = plugin_dir / "README_CN.md" if readme_cn.exists(): # Pattern: **版本:** 1.0.0 self.update_file_content( readme_cn, r"(\*\*版本:?\*\*\s*)([\d\.]+)", rf"\g<1>{true_version}", true_version, ) # 5. Check Global Docs Index (docs/plugins/{type}/index.md) index_md = self.docs_dir / plugin_type / "index.md" if index_md.exists(): # Need to find the specific block for this plugin. # This is harder with regex on the whole file. # We assume the format: **Version:** X.Y.Z # But we need to make sure we are updating the RIGHT plugin's version. # Strategy: Look for the plugin title or link, then the version nearby. # Extract title from py file to help search title = self.extract_title(py_file) if title: self.update_version_in_index(index_md, title, true_version) # 6. Check Global Docs Index CN (docs/plugins/{type}/index.zh.md) index_zh = self.docs_dir / plugin_type / "index.zh.md" if index_zh.exists(): # Try to find Chinese title? Or just use English title if listed? # Often Chinese index uses English title or Chinese title. # Let's try to extract Chinese title from cn_py if available cn_title = None if cn_py_files: cn_title = self.extract_title(cn_py_files[0]) target_title = cn_title if cn_title else title if target_title: self.update_version_in_index( index_zh, target_title, true_version, is_zh=True ) # 7. Check Global Detail Page (docs/plugins/{type}/{name}.md) # The doc filename usually matches the plugin directory name detail_md = self.docs_dir / plugin_type / f"{plugin_name}.md" if detail_md.exists(): self.update_file_content( detail_md, r'(v)([\d\.]+)()', rf"\g<1>{true_version}\g<3>", true_version, ) # 8. Check Global Detail Page CN (docs/plugins/{type}/{name}.zh.md) detail_zh = self.docs_dir / plugin_type / f"{plugin_name}.zh.md" if detail_zh.exists(): self.update_file_content( detail_zh, r'(v)([\d\.]+)()', rf"\g<1>{true_version}\g<3>", true_version, ) def extract_title(self, file_path: Path) -> Optional[str]: try: content = file_path.read_text(encoding="utf-8") match = re.search(r"title:\s*(.+)", content) if match: return match.group(1).strip() except: pass return None def update_version_in_index( self, file_path: Path, title: str, version: str, is_zh: bool = False ): """ Update version in index file. Look for: - ... **Title** ... - ... - **Version:** X.Y.Z """ try: content = file_path.read_text(encoding="utf-8") # Escape title for regex safe_title = re.escape(title) # Regex to find the plugin block and its version # We look for the title, then non-greedy match until we find Version line if is_zh: ver_label = r"\*\*版本:\*\*" else: ver_label = r"\*\*Version:\*\*" # Pattern: (Title ...)(Version: )(\d+\.\d+\.\d+) # We allow some lines between title and version pattern = rf"(\*\*{safe_title}\*\*[\s\S]*?{ver_label}\s*)([\d\.]+)" match = re.search(pattern, content) if match: current_ver = match.group(2) if current_ver != version: if self.fix: new_content = content.replace( match.group(0), f"{match.group(1)}{version}" ) file_path.write_text(new_content, encoding="utf-8") log_success( f"Fixed index for {title}: {current_ver} -> {version}" ) self.fixed_count += 1 else: log_error( f"Mismatch in index for {title}: Found {current_ver}, Expected {version}" ) self.issues_found += 1 else: # log_warning(f"Could not find entry for '{title}' in {file_path.name}") pass except Exception as e: log_error(f"Failed to check index {file_path}: {e}") def run(self): if not self.plugins_dir.exists(): log_error(f"Plugins directory not found: {self.plugins_dir}") return # Scan actions, filters, pipes for type_dir in self.plugins_dir.iterdir(): if type_dir.is_dir() and type_dir.name in ["actions", "filters", "pipes"]: for plugin_dir in type_dir.iterdir(): if plugin_dir.is_dir(): self.check_plugin(type_dir.name, plugin_dir) print("-" * 40) if self.issues_found > 0: if self.fix: print(f"Fixed {self.fixed_count} issues.") else: print(f"Found {self.issues_found} version inconsistencies.") print(f"Run with --fix to automatically resolve them.") sys.exit(1) else: print("All versions are consistent! ✨") def main(): parser = argparse.ArgumentParser(description="Check version consistency.") parser.add_argument("--fix", action="store_true", help="Fix inconsistencies") args = parser.parse_args() # Assume script is run from root or scripts dir root = Path.cwd() if (root / "scripts").exists(): pass elif root.name == "scripts": root = root.parent checker = VersionChecker(str(root), fix=args.fix) checker.run() if __name__ == "__main__": main()