121 lines
5.8 KiB
Python
121 lines
5.8 KiB
Python
"""Принудительное применение матрицы «пресет → настройки» на живой инсталляции.
|
||
|
||
Бутстрап (`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())
|