feat(dashboard): 4-way roster (user/bot/voice/role), persistence, TTS all-emotions, layout
- 유저 등록/관리 search now has 4 types: 유저(non-bot members), 봇(bot members), 통화방(current voice-channel members, bot flag enriched), 역할(roles — expandable to the members under each role; register the role OR a member under it). - Persist settings to a JSON state store (WSAI_STATE_FILE, default ~/.config/wsai/state.json) so they survive a service/container restart: per-guild listen lists + bargeIn (BotControl), TTS base/overrides and the chosen STT/LLM models (Dashboard). Applied on voice-server startup before warm. - TTS 감정 선택에 최상단 "전체변경 (모든 감정)" 추가: 적용하면 base를 그 값으로 세팅하고 모든 per-emotion override를 지워 전 감정이 한 번에 그 값으로 말합니다. - Move the component status bar (눈/시각/귀/LLM/입) above the bot bar. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
61
wsai/state_store.py
Normal file
61
wsai/state_store.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Tiny JSON state store so dashboard/bot settings survive a restart.
|
||||
|
||||
Persists things the user configures on the dashboard — per-guild listen lists,
|
||||
bot behaviour toggles, TTS controls, and the chosen STT/LLM models — to a single
|
||||
JSON file so a service or container restart keeps them.
|
||||
|
||||
Path: ``WSAI_STATE_FILE`` env, else ``~/.config/wsai/state.json``. For a
|
||||
container deployment, mount that path (or point the env at a mounted volume) to
|
||||
keep the file across ``docker restart``.
|
||||
|
||||
Pure stdlib, thread-safe, best-effort: a read/write failure never raises into
|
||||
the caller (the dashboard must keep working even if the disk is unwritable).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from typing import Any
|
||||
|
||||
log = logging.getLogger("wsai.state")
|
||||
|
||||
_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def path() -> str:
|
||||
return os.environ.get("WSAI_STATE_FILE") or os.path.expanduser("~/.config/wsai/state.json")
|
||||
|
||||
|
||||
def load() -> dict[str, Any]:
|
||||
try:
|
||||
with open(path(), encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return data if isinstance(data, dict) else {}
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
except Exception as exc: # noqa: BLE001 — corrupt/unreadable state must not crash startup
|
||||
log.warning("state load failed (%s): %s", path(), exc)
|
||||
return {}
|
||||
|
||||
|
||||
def save(state: dict[str, Any]) -> None:
|
||||
p = path()
|
||||
try:
|
||||
with _LOCK:
|
||||
os.makedirs(os.path.dirname(p), exist_ok=True)
|
||||
tmp = p + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(state, f, ensure_ascii=False, indent=2)
|
||||
os.replace(tmp, p) # atomic
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log.warning("state save failed (%s): %s", p, exc)
|
||||
|
||||
|
||||
def patch(key: str, value: Any) -> None:
|
||||
"""Read-modify-write one top-level key."""
|
||||
s = load()
|
||||
s[key] = value
|
||||
save(s)
|
||||
Reference in New Issue
Block a user