187 lines
6.7 KiB
Python
187 lines
6.7 KiB
Python
"""
|
|
Configuration module for ComfyUI Runtime Diagnostics.
|
|
Provides centralized, JSON-overridable configuration.
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import tempfile
|
|
from dataclasses import dataclass, asdict, field
|
|
from typing import Optional, List
|
|
# CRITICAL: keep relative-first fallback; custom-node package load does not
|
|
# guarantee the extension root is a top-level import root.
|
|
try:
|
|
from .services.config_guardrails import GuardrailConfig
|
|
except ImportError as import_error:
|
|
from import_compat import ensure_absolute_import_fallback_allowed
|
|
ensure_absolute_import_fallback_allowed(import_error)
|
|
from services.config_guardrails import GuardrailConfig
|
|
|
|
|
|
def _get_config_path_candidates() -> List[str]:
|
|
"""
|
|
Return config.json path candidates in priority order.
|
|
|
|
R18: Prefer canonical Doctor data dir for persisted config (Desktop-safe),
|
|
but keep backward-compatible fallback to the legacy extension-root config.json.
|
|
"""
|
|
candidates: List[str] = []
|
|
try:
|
|
try:
|
|
from .services.doctor_paths import get_doctor_data_dir # local import to avoid import-time coupling
|
|
except ImportError as import_error:
|
|
from import_compat import ensure_absolute_import_fallback_allowed
|
|
ensure_absolute_import_fallback_allowed(import_error)
|
|
from services.doctor_paths import get_doctor_data_dir
|
|
candidates.append(os.path.join(get_doctor_data_dir(), "config.json"))
|
|
except Exception:
|
|
pass
|
|
|
|
# Legacy location (extension root)
|
|
candidates.append(os.path.join(os.path.dirname(__file__), "config.json"))
|
|
|
|
# OS temp fallback (last resort; avoids writing under Desktop resources)
|
|
candidates.append(os.path.join(tempfile.gettempdir(), "ComfyUI-Doctor", "config.json"))
|
|
return candidates
|
|
|
|
|
|
def _get_primary_config_path() -> str:
|
|
return _get_config_path_candidates()[0]
|
|
|
|
|
|
@dataclass
|
|
class DiagnosticsConfig:
|
|
"""Central configuration for the diagnostics system."""
|
|
|
|
# Logging
|
|
max_log_files: int = 10
|
|
buffer_limit: int = 100
|
|
traceback_timeout_seconds: float = 5.0
|
|
log_queue_maxsize: int = 1000
|
|
|
|
# History
|
|
# history_size: 0 = unbounded, >0 = max entries
|
|
history_size: int = 0
|
|
# history_size_bytes: Max size in bytes for history file (default 5MB)
|
|
history_size_bytes: int = 5 * 1024 * 1024
|
|
|
|
# i18n
|
|
default_language: str = "zh_TW"
|
|
|
|
# Features
|
|
enable_web_panel: bool = True
|
|
enable_api: bool = True
|
|
|
|
# Plugins (P0 hardening)
|
|
enable_community_plugins: bool = False
|
|
plugin_allowlist: List[str] = field(default_factory=list)
|
|
plugin_blocklist: List[str] = field(default_factory=list)
|
|
plugin_max_file_size_bytes: int = 262144 # 256 KiB
|
|
plugin_max_scan_files: int = 50
|
|
plugin_reject_symlinks: bool = True
|
|
plugin_signature_required: bool = False
|
|
plugin_signature_key: str = ""
|
|
plugin_signature_alg: str = "hmac-sha256"
|
|
|
|
# Telemetry (S3)
|
|
telemetry_enabled: bool = False # Opt-in: disabled by default
|
|
|
|
# R7: Rate limiting
|
|
llm_core_rate_limit: int = 30 # req/min for analyze, chat
|
|
llm_light_rate_limit: int = 10 # req/min for verify_key, list_models
|
|
llm_max_concurrent: int = 3 # max simultaneous LLM requests
|
|
|
|
# R6: Retry configuration
|
|
llm_max_retries: int = 2
|
|
llm_request_timeout: float = 60.0 # per-attempt timeout (seconds)
|
|
llm_total_timeout: float = 180.0 # total operation timeout (seconds)
|
|
|
|
# S13: Outbound proxy trust boundary policy
|
|
# strict_off: ignore env proxy variables (secure default)
|
|
# inherit_env: allow aiohttp trust_env to inherit env proxy variables
|
|
llm_proxy_policy: str = "strict_off"
|
|
|
|
# R12: Token Budget Management
|
|
r12_enabled_remote: bool = True
|
|
r12_enabled_local: bool = False
|
|
r12_soft_max_tokens_remote: int = 4500
|
|
r12_hard_max_tokens_remote: int = 6000
|
|
r12_soft_max_tokens_local: int = 12000
|
|
r12_hard_max_tokens_local: int = 16000
|
|
r12_policy_profile: str = "remote_strict" # remote_strict, local_soft
|
|
r12_estimator_fallback_cpt: float = 4.0
|
|
r12_estimator_safety_mult: float = 1.15
|
|
r12_prune_default_depth: int = 3
|
|
r12_prune_default_nodes: int = 40
|
|
r12_overhead_fixed: int = 1000 # Fixed overhead (reserved tokens for structure)
|
|
|
|
# R14: Error Context Extraction & Prompt Optimization
|
|
r14_use_prompt_composer: bool = True # Use PromptComposer for unified context formatting
|
|
r14_use_legacy_format: bool = False # Fallback to legacy format (traceback-first)
|
|
|
|
# R17: Runtime Guardrails (env-driven)
|
|
# IMPORTANT: Runtime policy is ENV-driven and must never be persisted to config.json.
|
|
guardrails: GuardrailConfig = field(default_factory=GuardrailConfig.load)
|
|
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Convert config to dictionary."""
|
|
data = asdict(self)
|
|
# IMPORTANT: Keep guardrails runtime-only; persisted values would override ENV policy.
|
|
data.pop("guardrails", None)
|
|
return data
|
|
|
|
|
|
def load_config() -> DiagnosticsConfig:
|
|
"""
|
|
Load configuration from config.json if it exists,
|
|
otherwise return defaults.
|
|
"""
|
|
for config_path in _get_config_path_candidates():
|
|
if os.path.exists(config_path):
|
|
try:
|
|
with open(config_path, encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
if isinstance(data, dict):
|
|
# IMPORTANT: Ignore legacy/runtime guardrails if present in older config files.
|
|
data.pop("guardrails", None)
|
|
return DiagnosticsConfig(**data)
|
|
except Exception:
|
|
# Fall back to next candidate on any error
|
|
pass
|
|
return DiagnosticsConfig()
|
|
|
|
|
|
def save_config(config: DiagnosticsConfig) -> bool:
|
|
"""Save current configuration to config.json."""
|
|
payload = json.dumps(config.to_dict(), indent=2, ensure_ascii=False)
|
|
for config_path in _get_config_path_candidates():
|
|
try:
|
|
dir_path = os.path.dirname(config_path)
|
|
if dir_path:
|
|
os.makedirs(dir_path, exist_ok=True)
|
|
|
|
tmp_path = f"{config_path}.tmp"
|
|
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
f.write(payload)
|
|
f.flush()
|
|
try:
|
|
os.fsync(f.fileno())
|
|
except Exception:
|
|
pass
|
|
os.replace(tmp_path, config_path)
|
|
return True
|
|
except Exception:
|
|
try:
|
|
tmp_path = f"{config_path}.tmp"
|
|
if os.path.exists(tmp_path):
|
|
os.remove(tmp_path)
|
|
except Exception:
|
|
pass
|
|
continue
|
|
return False
|
|
|
|
|
|
# Global config instance
|
|
CONFIG = load_config()
|