feat: memory_store + profiles_store + 10 new yaml entries (119 → 129)
Mirrors `long_term_memory` and `profiles` modules from rust fork.
memory_store.py (new, ~95 lines)
- JSON store at <here>/long_term_memory.json (atomic write).
- remember(key, value) / recall(key) / forget(key) / search(query, limit) /
all_facts() / build_llm_context(prompt, limit).
- Uses the same JSON shape as the rust side, so the file is
interoperable if you ever symlink the two installations to the same dir.
profiles_store.py (new, ~145 lines)
- Profiles at <here>/profiles/<name>.json. active_profile.txt persists.
- Seeds 5 defaults on first init: default ★, work 💼, game 🎮, sleep 🌙,
driving 🚗 — same as rust fork.
- active() / active_name() / set_active(name) / list_names() /
allows_command(cmd_id).
extensions.py (+6 handlers, +160 lines)
- do_memory_remember — derives key (first 6 words or split on '=')
- do_memory_recall — substring search or top-3 if no query
- do_memory_forget — exact then substring fallback
- do_memory_list — count + first 5 keys
- do_profile_set — target via action.target or voice ("работа" → work)
- do_profile_what — speaks current profile + description
main.py
- +6 dispatch entries for the new action types.
commands.yaml (+10 entries, 119 → 129)
- memory_remember, memory_recall, memory_forget, memory_list
- profile_work, profile_game, profile_sleep, profile_driving, profile_default
- profile_what
Tests: ast.parse passes for all .py files. yaml.safe_load gives 129 entries.
Python parity is now meaningful: memory + profiles are usable by voice
without any rust install. Still missing for full parity: scheduler (needs
background thread), macros (needs phrase-capture state), vision (needs
Groq vision model call), codebase_qa, github_pr.
This commit is contained in:
parent
f82427c7b6
commit
02c6f2f90d
5 changed files with 521 additions and 0 deletions
|
|
@ -1033,3 +1033,83 @@ interesting_fact:
|
||||||
- интересный факт
|
- интересный факт
|
||||||
- расскажи факт
|
- расскажи факт
|
||||||
action: {type: interesting_fact}
|
action: {type: interesting_fact}
|
||||||
|
|
||||||
|
memory_remember:
|
||||||
|
phrases:
|
||||||
|
- запомни обо мне
|
||||||
|
- запомни что
|
||||||
|
- помни что
|
||||||
|
- запомни это
|
||||||
|
- запомни про меня
|
||||||
|
action: {type: memory_remember}
|
||||||
|
|
||||||
|
memory_recall:
|
||||||
|
phrases:
|
||||||
|
- что ты помнишь о
|
||||||
|
- что ты знаешь обо мне
|
||||||
|
- что помнишь про
|
||||||
|
- вспомни
|
||||||
|
action: {type: memory_recall}
|
||||||
|
|
||||||
|
memory_forget:
|
||||||
|
phrases:
|
||||||
|
- забудь про
|
||||||
|
- забудь что
|
||||||
|
- забудь обо мне
|
||||||
|
action: {type: memory_forget}
|
||||||
|
|
||||||
|
memory_list:
|
||||||
|
phrases:
|
||||||
|
- что ты помнишь
|
||||||
|
- что ты знаешь обо мне всё
|
||||||
|
- покажи память
|
||||||
|
action: {type: memory_list}
|
||||||
|
|
||||||
|
profile_work:
|
||||||
|
phrases:
|
||||||
|
- режим работа
|
||||||
|
- включи режим работы
|
||||||
|
- рабочий режим
|
||||||
|
- переключись в работу
|
||||||
|
action: {type: profile_set, target: work}
|
||||||
|
|
||||||
|
profile_game:
|
||||||
|
phrases:
|
||||||
|
- режим игра
|
||||||
|
- игровой режим
|
||||||
|
- включи игровой режим
|
||||||
|
- переключись в игры
|
||||||
|
action: {type: profile_set, target: game}
|
||||||
|
|
||||||
|
profile_sleep:
|
||||||
|
phrases:
|
||||||
|
- режим сон
|
||||||
|
- ночной режим
|
||||||
|
- режим тишины
|
||||||
|
- иди спать
|
||||||
|
- выключи всё
|
||||||
|
action: {type: profile_set, target: sleep}
|
||||||
|
|
||||||
|
profile_driving:
|
||||||
|
phrases:
|
||||||
|
- режим за рулём
|
||||||
|
- режим вождения
|
||||||
|
- я за рулём
|
||||||
|
- поехали
|
||||||
|
action: {type: profile_set, target: driving}
|
||||||
|
|
||||||
|
profile_default:
|
||||||
|
phrases:
|
||||||
|
- обычный режим
|
||||||
|
- режим по умолчанию
|
||||||
|
- сбрось режим
|
||||||
|
- вернись в обычный
|
||||||
|
action: {type: profile_set, target: default}
|
||||||
|
|
||||||
|
profile_what:
|
||||||
|
phrases:
|
||||||
|
- какой сейчас режим
|
||||||
|
- в каком ты режиме
|
||||||
|
- текущий режим
|
||||||
|
- какой режим
|
||||||
|
action: {type: profile_what}
|
||||||
|
|
|
||||||
136
extensions.py
136
extensions.py
|
|
@ -20,6 +20,9 @@ import uuid as uuidlib
|
||||||
import re
|
import re
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
|
import memory_store
|
||||||
|
import profiles_store
|
||||||
|
|
||||||
_speak_fn = print
|
_speak_fn = print
|
||||||
_set_clipboard_fn = None
|
_set_clipboard_fn = None
|
||||||
|
|
||||||
|
|
@ -445,6 +448,139 @@ def do_self_ping(action, voice):
|
||||||
_speak(random.choice(options))
|
_speak(random.choice(options))
|
||||||
|
|
||||||
|
|
||||||
|
# ── memory ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_MEMORY_REMEMBER_TRIGGERS = (
|
||||||
|
'запомни обо мне что', 'запомни обо мне', 'запомни что',
|
||||||
|
'помни что', 'запомни это', 'запомни про меня', 'запомни',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def do_memory_remember(action, voice):
|
||||||
|
body = (voice or '').strip()
|
||||||
|
low = body.lower()
|
||||||
|
for trig in _MEMORY_REMEMBER_TRIGGERS:
|
||||||
|
if low.startswith(trig):
|
||||||
|
body = body[len(trig):].strip(' ,.:')
|
||||||
|
break
|
||||||
|
|
||||||
|
if not body:
|
||||||
|
_speak("Что именно запомнить?")
|
||||||
|
return
|
||||||
|
|
||||||
|
if '=' in body:
|
||||||
|
key, value = body.split('=', 1)
|
||||||
|
key, value = key.strip(), value.strip()
|
||||||
|
else:
|
||||||
|
words = body.split()
|
||||||
|
key = ' '.join(words[:6])
|
||||||
|
value = body
|
||||||
|
|
||||||
|
if not key:
|
||||||
|
_speak("Не понял ключ.")
|
||||||
|
return
|
||||||
|
|
||||||
|
memory_store.remember(key, value)
|
||||||
|
_speak("Запомнил.")
|
||||||
|
|
||||||
|
|
||||||
|
def do_memory_recall(action, voice):
|
||||||
|
body = (voice or '').lower()
|
||||||
|
query = ''
|
||||||
|
for trig in ('что ты помнишь о', 'что ты помнишь про', 'что ты знаешь обо мне',
|
||||||
|
'что помнишь про', 'вспомни'):
|
||||||
|
idx = body.find(trig)
|
||||||
|
if idx >= 0:
|
||||||
|
query = body[idx + len(trig):].strip(' ,.:')
|
||||||
|
break
|
||||||
|
|
||||||
|
if not query:
|
||||||
|
recs = memory_store.all_facts()
|
||||||
|
if not recs:
|
||||||
|
_speak("Ничего пока не помню.")
|
||||||
|
return
|
||||||
|
line = "Я помню: "
|
||||||
|
for r in recs[:3]:
|
||||||
|
line += f"{r['key']} — {r['value']}. "
|
||||||
|
_speak(line)
|
||||||
|
return
|
||||||
|
|
||||||
|
hits = memory_store.search(query, 3)
|
||||||
|
if not hits:
|
||||||
|
_speak(f"Ничего не нашёл про {query}.")
|
||||||
|
return
|
||||||
|
if len(hits) == 1:
|
||||||
|
_speak(hits[0]['value'])
|
||||||
|
else:
|
||||||
|
_speak('. '.join(f"{h['key']}: {h['value']}" for h in hits))
|
||||||
|
|
||||||
|
|
||||||
|
def do_memory_forget(action, voice):
|
||||||
|
body = (voice or '').lower()
|
||||||
|
for trig in ('забудь про', 'забудь что', 'забудь обо мне'):
|
||||||
|
idx = body.find(trig)
|
||||||
|
if idx >= 0:
|
||||||
|
key = body[idx + len(trig):].strip(' ,.:')
|
||||||
|
if memory_store.forget(key):
|
||||||
|
_speak(f"Забыл про {key}.")
|
||||||
|
return
|
||||||
|
hits = memory_store.search(key, 1)
|
||||||
|
if hits:
|
||||||
|
memory_store.forget(hits[0]['key'])
|
||||||
|
_speak(f"Забыл про {hits[0]['key']}.")
|
||||||
|
return
|
||||||
|
_speak(f"Я и не помнил про {key}.")
|
||||||
|
return
|
||||||
|
_speak("Что забыть?")
|
||||||
|
|
||||||
|
|
||||||
|
def do_memory_list(action, voice):
|
||||||
|
recs = memory_store.all_facts()
|
||||||
|
if not recs:
|
||||||
|
_speak("Я пока ничего о вас не запомнил.")
|
||||||
|
return
|
||||||
|
sample = min(len(recs), 5)
|
||||||
|
line = f"Помню {len(recs)} фактов. Первые: "
|
||||||
|
line += ", ".join(r['key'] for r in recs[:sample]) + "."
|
||||||
|
_speak(line)
|
||||||
|
|
||||||
|
|
||||||
|
# ── profile switch ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def do_profile_set(action, voice):
|
||||||
|
target = action.get('target') or _profile_target_from_voice(voice)
|
||||||
|
if not target:
|
||||||
|
_speak("Не понял какой режим.")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
p = profiles_store.set_active(target)
|
||||||
|
except ValueError:
|
||||||
|
_speak(f"Профиль {target} не найден.")
|
||||||
|
return
|
||||||
|
greeting = p.get('greeting', '') or f"Режим {target}."
|
||||||
|
_speak(greeting)
|
||||||
|
|
||||||
|
|
||||||
|
def _profile_target_from_voice(voice):
|
||||||
|
low = (voice or '').lower()
|
||||||
|
if 'работ' in low: return 'work'
|
||||||
|
if 'игр' in low: return 'game'
|
||||||
|
if 'сон' in low or 'спать' in low or 'тиш' in low: return 'sleep'
|
||||||
|
if 'руль' in low or 'вожд' in low or 'поехали' in low: return 'driving'
|
||||||
|
if 'обычн' in low or 'умолчан' in low or 'сброс' in low or 'верн' in low: return 'default'
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def do_profile_what(action, voice):
|
||||||
|
p = profiles_store.active()
|
||||||
|
name = p.get('name', 'default')
|
||||||
|
desc = p.get('description', '')
|
||||||
|
msg = f"Сейчас {name}."
|
||||||
|
if desc:
|
||||||
|
msg += " " + desc
|
||||||
|
_speak(msg)
|
||||||
|
|
||||||
|
|
||||||
# ── interesting fact (LLM-dependent) ───────────────────────────────────────
|
# ── interesting fact (LLM-dependent) ───────────────────────────────────────
|
||||||
|
|
||||||
def do_interesting_fact(action, voice):
|
def do_interesting_fact(action, voice):
|
||||||
|
|
|
||||||
12
main.py
12
main.py
|
|
@ -1575,6 +1575,18 @@ def run_action(action):
|
||||||
extensions.do_self_ping(action, _current_voice or '')
|
extensions.do_self_ping(action, _current_voice or '')
|
||||||
elif t == 'interesting_fact':
|
elif t == 'interesting_fact':
|
||||||
extensions.do_interesting_fact(action, _current_voice or '')
|
extensions.do_interesting_fact(action, _current_voice or '')
|
||||||
|
elif t == 'memory_remember':
|
||||||
|
extensions.do_memory_remember(action, _current_voice or '')
|
||||||
|
elif t == 'memory_recall':
|
||||||
|
extensions.do_memory_recall(action, _current_voice or '')
|
||||||
|
elif t == 'memory_forget':
|
||||||
|
extensions.do_memory_forget(action, _current_voice or '')
|
||||||
|
elif t == 'memory_list':
|
||||||
|
extensions.do_memory_list(action, _current_voice or '')
|
||||||
|
elif t == 'profile_set':
|
||||||
|
extensions.do_profile_set(action, _current_voice or '')
|
||||||
|
elif t == 'profile_what':
|
||||||
|
extensions.do_profile_what(action, _current_voice or '')
|
||||||
|
|
||||||
|
|
||||||
_current_voice: str = ''
|
_current_voice: str = ''
|
||||||
|
|
|
||||||
125
memory_store.py
Normal file
125
memory_store.py
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
"""Long-term memory store — port of `crates/jarvis-core/src/long_term_memory.rs`.
|
||||||
|
|
||||||
|
Stores user facts in JSON at `<script_dir>/long_term_memory.json`.
|
||||||
|
Atomic write-through (write-then-rename).
|
||||||
|
|
||||||
|
Public API:
|
||||||
|
remember(key, value) -> None
|
||||||
|
recall(key) -> str | None
|
||||||
|
forget(key) -> bool
|
||||||
|
search(query, limit=5) -> list[dict]
|
||||||
|
all_facts() -> list[dict]
|
||||||
|
build_llm_context(prompt, limit=5) -> str # used by chat handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
# JSON file lives in the same folder as main.py so the user can move the
|
||||||
|
# install and the data follows.
|
||||||
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
_PATH = os.path.join(_HERE, 'long_term_memory.json')
|
||||||
|
|
||||||
|
_store: dict[str, dict] = {}
|
||||||
|
_loaded = False
|
||||||
|
|
||||||
|
|
||||||
|
def _load():
|
||||||
|
global _store, _loaded
|
||||||
|
if _loaded:
|
||||||
|
return
|
||||||
|
if os.path.isfile(_PATH):
|
||||||
|
try:
|
||||||
|
with open(_PATH, encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
_store = data.get('entries', {}) if isinstance(data, dict) else {}
|
||||||
|
except (OSError, json.JSONDecodeError) as exc:
|
||||||
|
print(f"[memory] corrupt store ({exc}) — starting empty")
|
||||||
|
_store = {}
|
||||||
|
_loaded = True
|
||||||
|
|
||||||
|
|
||||||
|
def _save():
|
||||||
|
tmp = _PATH + '.tmp'
|
||||||
|
try:
|
||||||
|
with open(tmp, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump({'entries': _store}, f, ensure_ascii=False, indent=2)
|
||||||
|
os.replace(tmp, _PATH)
|
||||||
|
except OSError as exc:
|
||||||
|
print(f"[memory] save failed: {exc}")
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_key(k: str) -> str:
|
||||||
|
return (k or '').strip().lower()
|
||||||
|
|
||||||
|
|
||||||
|
def remember(key: str, value: str) -> None:
|
||||||
|
_load()
|
||||||
|
nk = _normalize_key(key)
|
||||||
|
if not nk:
|
||||||
|
return
|
||||||
|
now = int(time.time())
|
||||||
|
if nk in _store:
|
||||||
|
_store[nk]['value'] = value
|
||||||
|
_store[nk]['last_used_at'] = now
|
||||||
|
else:
|
||||||
|
_store[nk] = {
|
||||||
|
'key': nk,
|
||||||
|
'value': value,
|
||||||
|
'created_at': now,
|
||||||
|
'last_used_at': now,
|
||||||
|
'use_count': 0,
|
||||||
|
}
|
||||||
|
_save()
|
||||||
|
|
||||||
|
|
||||||
|
def recall(key: str) -> str | None:
|
||||||
|
_load()
|
||||||
|
nk = _normalize_key(key)
|
||||||
|
entry = _store.get(nk)
|
||||||
|
if not entry:
|
||||||
|
return None
|
||||||
|
entry['last_used_at'] = int(time.time())
|
||||||
|
entry['use_count'] = entry.get('use_count', 0) + 1
|
||||||
|
_save()
|
||||||
|
return entry['value']
|
||||||
|
|
||||||
|
|
||||||
|
def forget(key: str) -> bool:
|
||||||
|
_load()
|
||||||
|
nk = _normalize_key(key)
|
||||||
|
if nk in _store:
|
||||||
|
del _store[nk]
|
||||||
|
_save()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def search(query: str, limit: int = 5) -> list[dict]:
|
||||||
|
_load()
|
||||||
|
nq = _normalize_key(query)
|
||||||
|
if not nq:
|
||||||
|
return []
|
||||||
|
hits = [
|
||||||
|
e for e in _store.values()
|
||||||
|
if nq in e['key'] or nq in e['value'].lower()
|
||||||
|
]
|
||||||
|
hits.sort(key=lambda e: e.get('last_used_at', 0), reverse=True)
|
||||||
|
return hits[: max(1, limit)]
|
||||||
|
|
||||||
|
|
||||||
|
def all_facts() -> list[dict]:
|
||||||
|
_load()
|
||||||
|
return list(_store.values())
|
||||||
|
|
||||||
|
|
||||||
|
def build_llm_context(prompt: str, limit: int = 5) -> str:
|
||||||
|
"""Format relevant memory facts as a system-prompt addendum."""
|
||||||
|
hits = search(prompt, limit)
|
||||||
|
if not hits:
|
||||||
|
return ''
|
||||||
|
lines = ["Известные факты о пользователе (используй если уместно):"]
|
||||||
|
for h in hits:
|
||||||
|
lines.append(f"- {h['key']} = {h['value']}")
|
||||||
|
return '\n'.join(lines) + '\n'
|
||||||
168
profiles_store.py
Normal file
168
profiles_store.py
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
"""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)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue