Первоначальная версия VidConf
This commit is contained in:
0
backend/scripts/__init__.py
Normal file
0
backend/scripts/__init__.py
Normal file
120
backend/scripts/apply_preset_settings.py
Normal file
120
backend/scripts/apply_preset_settings.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""Принудительное применение матрицы «пресет → настройки» на живой инсталляции.
|
||||
|
||||
Бутстрап (`main.py::lifespan`,
|
||||
`InstanceSettingsService.ensure_bootstrapped`) применяет overrides пресета
|
||||
инсталлятора (`BOOTSTRAP_CHAT_ENABLED`/`BOOTSTRAP_TRANSCRIPTION_ENABLED`/
|
||||
`BOOTSTRAP_AI_LEVEL` в `.env`, см. `core.config.Settings`) только на чистой
|
||||
БД (`INSERT ... ON CONFLICT DO NOTHING`) — строки уже существующей
|
||||
инсталляции он не трогает. Этот скрипт запускает install.sh при ПОВТОРНОМ
|
||||
запуске с другим пресетом на живой инсталляции, после подтверждения в
|
||||
опроснике («обновить настройки модулей под пресет?») либо флага `--yes`
|
||||
самого install.sh.
|
||||
|
||||
Управляет РОВНО 4 ключами (`chat`, `transcriber`, `summarizer`, `ai_level`,
|
||||
см. `services.instance_settings.BOOTSTRAP_MANAGED_KEYS`) — остальные
|
||||
настройки инстанса (таймзона, рассылка саммари, регистрация) этот скрипт не
|
||||
трогает. Без `--force` пишет только ОТСУТСТВУЮЩИЕ из них — страховка от
|
||||
молчаливой потери ручных правок администратора; с `--force` перезаписывает
|
||||
все 4 значениями текущего пресета (`transcriber`/`summarizer` — целиком, тем
|
||||
же дефолтом `plugins.yaml` + `enabled`, что и обычный бутстрап: подмена
|
||||
provider/model для уровней `medium`/`max` происходит при чтении конфигурации,
|
||||
см. `services.instance_settings.load_effective_config`, а не при записи).
|
||||
|
||||
`ai_level` пишется напрямую в БД, минуя валидацию доступности
|
||||
(`services.ai_levels.detect_ai_levels`): модели уровня `medium`/`max`
|
||||
докачиваются отдельными профилями `docker-compose` уже ПОСЛЕ первого запуска
|
||||
install.sh, и жёсткая валидация здесь давала бы ложный отказ применения
|
||||
пресета, пока докачка не завершена — вместо этого пишем предупреждение в лог.
|
||||
|
||||
Запустить: `uv run python -m scripts.apply_preset_settings [--force]`
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
|
||||
from core.config import get_settings
|
||||
from core.db import async_session_maker, engine
|
||||
from core.plugins.config import load_plugins_config
|
||||
from models.instance_setting import InstanceSetting
|
||||
from services.instance_settings import (
|
||||
BOOTSTRAP_MANAGED_KEYS,
|
||||
bootstrap_overrides_from_settings,
|
||||
build_bootstrap_defaults,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def apply_preset_settings(*, force: bool) -> list[str]:
|
||||
"""Применить текущий пресет (`BOOTSTRAP_*`) к управляемым ключам `instance_settings`.
|
||||
|
||||
Возвращает список фактически записанных ключей (пустой — писать было
|
||||
нечего: все 4 ключа уже существуют и `force=False`).
|
||||
"""
|
||||
settings = get_settings()
|
||||
plugins = load_plugins_config(settings.plugins_config_path)
|
||||
overrides = bootstrap_overrides_from_settings(settings)
|
||||
defaults = build_bootstrap_defaults(plugins, overrides)
|
||||
|
||||
async with async_session_maker() as session:
|
||||
result = await session.execute(
|
||||
select(InstanceSetting.key).where(InstanceSetting.key.in_(BOOTSTRAP_MANAGED_KEYS))
|
||||
)
|
||||
existing_keys = set(result.scalars().all())
|
||||
|
||||
written: list[str] = []
|
||||
for key in BOOTSTRAP_MANAGED_KEYS:
|
||||
if key in existing_keys and not force:
|
||||
continue
|
||||
value = defaults[key]
|
||||
stmt = (
|
||||
pg_insert(InstanceSetting)
|
||||
.values(key=key, value=value)
|
||||
.on_conflict_do_update(index_elements=["key"], set_={"value": value})
|
||||
)
|
||||
await session.execute(stmt)
|
||||
written.append(key)
|
||||
await session.commit()
|
||||
|
||||
if "ai_level" in written:
|
||||
logger.warning(
|
||||
"apply_preset_settings: ai_level=%r записан напрямую, без проверки доступности "
|
||||
"(detect_ai_levels) — модели уровня могут ещё докачиваться профилем docker-compose",
|
||||
defaults["ai_level"]["level"],
|
||||
)
|
||||
logger.info(
|
||||
"apply_preset_settings: обновлены ключи %s (force=%s, пропущены как уже существующие: %s)",
|
||||
written or "нет",
|
||||
force,
|
||||
sorted(existing_keys - set(written)) or "нет",
|
||||
)
|
||||
return written
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Применить матрицу «пресет → настройки» инсталлятора к instance_settings."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="перезаписать уже существующие ключи (без флага пишутся только отсутствующие)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
async def _main() -> None:
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s"
|
||||
)
|
||||
args = _parse_args()
|
||||
await apply_preset_settings(force=args.force)
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(_main())
|
||||
51
backend/scripts/seed.py
Normal file
51
backend/scripts/seed.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""Идемпотентный seed скрипт: единственный админ-пользователь.
|
||||
|
||||
Комнаты (100 демо-комнат) больше не существуют как сущность (ADR-001) —
|
||||
seed сокращён до создания администратора.
|
||||
|
||||
Запустить с: `uv run python -m scripts.seed`
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from argon2 import PasswordHasher
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.config import get_settings
|
||||
from core.db import async_session_maker, engine
|
||||
from models.user import User
|
||||
|
||||
_hasher = PasswordHasher()
|
||||
|
||||
|
||||
async def _seed_admin(session: AsyncSession) -> None:
|
||||
settings = get_settings()
|
||||
email = settings.seed_admin_email
|
||||
password = settings.seed_admin_password
|
||||
|
||||
existing = await session.scalar(select(User).where(User.email == email))
|
||||
if existing is not None:
|
||||
return
|
||||
|
||||
session.add(
|
||||
User(
|
||||
email=email,
|
||||
name_user="Admin",
|
||||
password_hash=_hasher.hash(password),
|
||||
role="admin",
|
||||
email_verified=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def seed() -> None:
|
||||
"""Upsert админ-пользователя; безопасно запускать несколько раз."""
|
||||
async with async_session_maker() as session:
|
||||
await _seed_admin(session)
|
||||
await session.commit()
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed())
|
||||
Reference in New Issue
Block a user