169 lines
5.6 KiB
Python
169 lines
5.6 KiB
Python
|
|
"""Profile switching — port of `crates/jarvis-core/src/profiles.rs`.
|
||
|
|
|
||
|
|
Profiles live in `<script_dir>/profiles/<name>.json`. Active profile name
|
||
|
|
persists in `<script_dir>/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)
|