Files
rays 8537b1b082 refactor: remove unused frontend modules, helpers and a stray script
Remove code that nothing imports or calls:

- web/doctor_chat.js (the ChatPanel class, superseded by the chat
  island; only its import in doctor_ui.js remained), web/doctor_chat.css
  and web/doctor_telemetry.js. ComfyUI still loaded these files as
  extension scripts, but they had no module-level side effects.
- DoctorUI.triggerAIAnalysis and DoctorUI.updatePollInterval;
  DoctorAPI.getHistory, DoctorAPI.verifyApiKey and DoctorAPI.chat;
  renderContentPipeline.
- The first, shadowed PatternLoader.get_pattern_info definition (the
  later one was always the one in effect).
- verify_routes.py, which could not run under package imports and
  replaced sys.modules entries with mocks when imported.

The architecture guide lists only existing frontend modules, enforced by
a new test, and the import-scan skip lists no longer name the script.
2026-10-13 14:45:04 +08:00

341 lines
12 KiB
Python

"""
JSON-based Error Pattern Loader with Hot-Reload Support.
Loads error pattern definitions from JSON files and provides pattern matching
with priority-based ordering and hot-reload capability.
Architecture:
- Load patterns from multiple JSON directories (builtin, community, custom)
- Priority-based sorting (higher priority patterns checked first)
- Hot-reload support (detects file changes without restart)
- Schema validation for pattern files
- Graceful fallback on loading errors
Usage:
loader = get_pattern_loader()
result = loader.match(error_traceback)
if result:
error_key, captured_groups = result
"""
import json
import re
import os
import logging
from pathlib import Path
from typing import List, Dict, Tuple, Optional
# Relative import with fallback for tests
try:
pass # No internal dependencies yet
except ImportError:
pass
logger = logging.getLogger(__name__)
class PatternLoader:
"""
JSON-based error pattern loader with hot-reload capability.
Features:
- Load patterns from multiple JSON files
- Priority-based sorting (high priority checked first)
- Hot-reload without restart
- Schema validation
- Merge strategy for community patterns
"""
def __init__(self, pattern_dirs: List[str], optional_pattern_dirs: Optional[List[str]] = None):
"""
Initialize loader with pattern directories.
Args:
pattern_dirs: List of directories to search for *.json files
Priority order: builtin.json > community.json > custom.json
optional_pattern_dirs: Entries of pattern_dirs that may be absent (for example a
user's custom directory); a missing one is logged at debug level only.
"""
self.pattern_dirs = [Path(d) for d in pattern_dirs]
self.optional_pattern_dirs = frozenset(Path(d) for d in (optional_pattern_dirs or []))
self.patterns: List[Dict] = []
self.compiled_patterns: List[Tuple[re.Pattern, str, bool, int]] = []
self._file_mtimes: Dict[Path, float] = {}
def load(self, validate_schema: bool = True) -> int:
"""
Load all patterns from JSON files.
Args:
validate_schema: Whether to validate JSON schema (default: True)
Returns:
Number of patterns loaded
Raises:
May log errors but never raises (graceful degradation)
"""
all_patterns = []
# Load from each directory
for pattern_dir in self.pattern_dirs:
if not pattern_dir.exists():
if pattern_dir in self.optional_pattern_dirs:
logger.debug(f"[PatternLoader] Optional pattern directory not present: {pattern_dir}")
else:
logger.warning(f"[PatternLoader] Pattern directory not found: {pattern_dir}")
continue
for json_file in pattern_dir.glob("*.json"):
try:
with open(json_file, 'r', encoding='utf-8') as f:
data = json.load(f)
# Schema validation
if validate_schema:
self._validate_schema(data, json_file)
# Track file modification time for hot-reload
self._file_mtimes[json_file] = json_file.stat().st_mtime
# Add patterns with source tracking
for pattern in data.get("patterns", []):
pattern["_source"] = str(json_file)
all_patterns.append(pattern)
logger.info(f"[PatternLoader] Loaded {len(data.get('patterns', []))} patterns from {json_file.name}")
except json.JSONDecodeError as e:
logger.error(f"[PatternLoader] Invalid JSON in {json_file}: {e}")
except Exception as e:
logger.error(f"[PatternLoader] Error loading {json_file}: {e}")
# Sort by priority (descending - higher priority first)
all_patterns.sort(key=lambda p: p.get("priority", 50), reverse=True)
# Compile regex patterns
self.patterns = all_patterns
self.compiled_patterns = []
for pattern in all_patterns:
try:
compiled = re.compile(pattern["regex"])
self.compiled_patterns.append((
compiled,
pattern["error_key"],
pattern.get("has_groups", False),
pattern.get("priority", 50)
))
except re.error as e:
logger.error(f"[PatternLoader] Invalid regex in pattern '{pattern.get('id', 'unknown')}': {e}")
logger.info(f"[PatternLoader] Total patterns loaded: {len(self.compiled_patterns)}")
return len(self.compiled_patterns)
def _validate_schema(self, data: dict, filepath: Path):
"""
Validate JSON against schema.
Args:
data: Parsed JSON data
filepath: Path to JSON file (for error messages)
Raises:
ValueError: If validation fails
"""
# Check required top-level fields
if "version" not in data:
raise ValueError(f"Missing 'version' in {filepath}")
if "patterns" not in data:
raise ValueError(f"Missing 'patterns' in {filepath}")
# Validate each pattern
seen_ids = set()
for i, pattern in enumerate(data["patterns"]):
# Required fields
required = {"id", "regex", "error_key", "priority", "category"}
missing = required - set(pattern.keys())
if missing:
raise ValueError(f"Pattern #{i} in {filepath} missing fields: {missing}")
# Unique IDs
pattern_id = pattern["id"]
if pattern_id in seen_ids:
raise ValueError(f"Duplicate pattern ID '{pattern_id}' in {filepath}")
seen_ids.add(pattern_id)
# Valid priority range
priority = pattern["priority"]
if not (0 <= priority <= 100):
raise ValueError(f"Pattern '{pattern_id}' priority {priority} out of range [0, 100]")
# Valid category
valid_categories = {
"memory", "model_loading", "custom_nodes",
"framework", "workflow", "data_type", "generic"
}
if pattern["category"] not in valid_categories:
raise ValueError(f"Pattern '{pattern_id}' has invalid category '{pattern['category']}'")
def reload_if_changed(self) -> bool:
"""
Check if any pattern files have been modified and reload if needed.
Returns:
True if patterns were reloaded, False otherwise
"""
changed = False
for pattern_dir in self.pattern_dirs:
if not pattern_dir.exists():
continue
for json_file in pattern_dir.glob("*.json"):
try:
current_mtime = json_file.stat().st_mtime
if json_file not in self._file_mtimes or current_mtime > self._file_mtimes[json_file]:
changed = True
break
except Exception as e:
logger.warning(f"[PatternLoader] Error checking {json_file}: {e}")
if changed:
logger.info("[PatternLoader] Pattern files changed, reloading...")
self.load()
return True
return False
def match(self, traceback_text: str) -> Optional[Tuple[str, Tuple]]:
"""
Match traceback against patterns.
Args:
traceback_text: Error traceback string
Returns:
Tuple of (error_key, captured_groups) or None if no match
"""
for compiled, error_key, has_groups, _ in self.compiled_patterns:
match = compiled.search(traceback_text)
if match:
if has_groups:
return (error_key, match.groups())
else:
return (error_key, [])
return None
def get_pattern_info(self, pattern_id: str) -> Optional[Dict]:
"""
Get full pattern metadata for a given pattern ID.
Args:
pattern_id: The pattern ID (error_key) to look up
Returns:
Dictionary with pattern metadata (id, category, priority, regex, etc.),
or None if pattern not found
"""
for pattern in self.patterns:
if pattern.get("error_key") == pattern_id or pattern.get("id") == pattern_id:
return pattern
return None
def get_pattern_ids(self) -> List[str]:
"""Return the ids of all loaded patterns, in match-priority order."""
return [pattern["id"] for pattern in self.patterns if pattern.get("id")]
def get_stats(self) -> Dict:
"""
Get pattern statistics.
Returns:
Dictionary with pattern statistics:
{
"total": int,
"by_category": {"memory": 5, ...},
"by_priority": {"high": 15, "medium": 20, ...},
"sources": ["builtin.json", ...]
}
"""
by_category = {}
by_priority = {"high": 0, "medium": 0, "low": 0}
sources = set()
for pattern in self.patterns:
# Category count
category = pattern.get("category", "generic")
by_category[category] = by_category.get(category, 0) + 1
# Priority buckets
priority = pattern.get("priority", 50)
if priority >= 80:
by_priority["high"] += 1
elif priority >= 50:
by_priority["medium"] += 1
else:
by_priority["low"] += 1
# Source files
if "_source" in pattern:
sources.add(Path(pattern["_source"]).name)
return {
"total": len(self.patterns),
"by_category": by_category,
"by_priority": by_priority,
"sources": sorted(sources)
}
# ==============================================================================
# Global Instance Management
# ==============================================================================
_pattern_loader: Optional[PatternLoader] = None
def get_pattern_loader() -> PatternLoader:
"""
Get global PatternLoader instance (singleton pattern).
Returns:
PatternLoader instance
Note:
Automatically initializes with default pattern directories on first call.
"""
global _pattern_loader
if _pattern_loader is None:
# Initialize with default pattern directories
base_dir = os.path.dirname(os.path.abspath(__file__))
custom_dir = os.path.join(base_dir, "patterns", "custom")
pattern_dirs = [
os.path.join(base_dir, "patterns", "builtin"),
os.path.join(base_dir, "patterns", "community"),
custom_dir
]
# The custom directory is user-provided and absent by default.
_pattern_loader = PatternLoader(pattern_dirs, optional_pattern_dirs=[custom_dir])
try:
count = _pattern_loader.load()
logger.info(f"[PatternLoader] Initialized with {count} patterns")
except Exception as e:
logger.error(f"[PatternLoader] Initialization failed: {e}")
# Return instance anyway - caller will use fallback
return _pattern_loader
def reset_pattern_loader():
"""
Reset global PatternLoader instance.
Useful for testing or forcing reload of all patterns.
"""
global _pattern_loader
_pattern_loader = None