"""Profile switching — port of `crates/jarvis-core/src/profiles.rs`. Profiles live in `/profiles/.json`. Active profile name persists in `/active_profile.txt`. Seeds 5 defaults on first run. Public API: active() -> dict # currently selected profile set_active(name) -> dict # raises ValueError if profile missing list_names() -> list[str] allows_command(cmd_id) -> bool """ import json import os _HERE = os.path.dirname(os.path.abspath(__file__)) _PROFILES_DIR = os.path.join(_HERE, 'profiles') _ACTIVE_FILE = os.path.join(_HERE, 'active_profile.txt') _active_cache: dict | None = None def _seed_defaults(): defaults = [ { 'name': 'default', 'description': 'Обычный режим, все команды доступны.', 'llm_personality': '', 'allowed_command_prefixes': [], 'disabled_command_prefixes': [], 'greeting': '', 'icon': '★', }, { 'name': 'work', 'description': 'Рабочий режим — фокус, отключены развлечения.', 'llm_personality': 'Отвечай кратко и по делу. Минимум шуток.', 'allowed_command_prefixes': [], 'disabled_command_prefixes': ['games.', 'fun.', 'music.'], 'greeting': 'Режим работы. Сосредоточимся.', 'icon': '💼', }, { 'name': 'game', 'description': 'Игровой режим — голосовые макросы и игры.', 'llm_personality': 'Будь дружелюбен. Краткие ответы. Игровая атмосфера.', 'allowed_command_prefixes': [], 'disabled_command_prefixes': ['reminders.', 'calendar.'], 'greeting': 'Игровой режим активирован.', 'icon': '🎮', }, { 'name': 'sleep', 'description': 'Спокойный режим — тихий голос, никаких уведомлений.', 'llm_personality': 'Говори спокойно, короткими фразами.', 'allowed_command_prefixes': ['time.', 'weather.', 'lights.'], 'disabled_command_prefixes': [], 'greeting': 'Спокойной ночи.', 'icon': '🌙', }, { 'name': 'driving', 'description': 'Режим за рулём — только безопасные команды.', 'llm_personality': 'Краткие безопасные ответы. Никаких длинных текстов.', 'allowed_command_prefixes': [ 'music.', 'navigation.', 'calls.', 'time.', 'weather.', 'reminders.', ], 'disabled_command_prefixes': ['windows.', 'screen.'], 'greeting': 'Режим за рулём. Веди безопасно.', 'icon': '🚗', }, ] os.makedirs(_PROFILES_DIR, exist_ok=True) for p in defaults: path = os.path.join(_PROFILES_DIR, f"{p['name']}.json") if not os.path.isfile(path): try: with open(path, 'w', encoding='utf-8') as f: json.dump(p, f, ensure_ascii=False, indent=2) except OSError as exc: print(f"[profiles] seed {p['name']} failed: {exc}") def _default_profile() -> dict: return { 'name': 'default', 'description': 'fallback', 'llm_personality': '', 'allowed_command_prefixes': [], 'disabled_command_prefixes': [], 'greeting': '', 'icon': '★', } def _load_profile(name: str) -> dict | None: path = os.path.join(_PROFILES_DIR, f'{name}.json') if not os.path.isfile(path): return None try: with open(path, encoding='utf-8') as f: return json.load(f) except (OSError, json.JSONDecodeError) as exc: print(f"[profiles] load {name} failed: {exc}") return None def _read_active_name() -> str: try: with open(_ACTIVE_FILE, encoding='utf-8') as f: return f.read().strip() or 'default' except OSError: return 'default' def _init(): global _active_cache if _active_cache is not None: return _seed_defaults() name = _read_active_name() _active_cache = _load_profile(name) or _default_profile() def active() -> dict: _init() return dict(_active_cache or _default_profile()) def active_name() -> str: return active().get('name', 'default') def set_active(name: str) -> dict: global _active_cache _init() profile = _load_profile(name) if not profile: raise ValueError(f"profile '{name}' not found") _active_cache = profile try: with open(_ACTIVE_FILE, 'w', encoding='utf-8') as f: f.write(name) except OSError as exc: print(f"[profiles] persist failed: {exc}") return dict(profile) def list_names() -> list[str]: _init() names = [] if os.path.isdir(_PROFILES_DIR): for fname in os.listdir(_PROFILES_DIR): if fname.endswith('.json'): names.append(fname[:-5]) return sorted(names) def allows_command(cmd_id: str) -> bool: p = active() disabled = p.get('disabled_command_prefixes', []) or [] if any(cmd_id.startswith(pfx) for pfx in disabled): return False allowed = p.get('allowed_command_prefixes', []) or [] if not allowed: return True return any(cmd_id.startswith(pfx) for pfx in allowed)