Отключаемый модуль (instance_settings.consent_policy): галочка + ссылка на публичную страницу регламента на форме регистрации, редактируемый в админке текст с типовым шаблоном по умолчанию (плейсхолдеры под организацию, не проходил юридическую проверку), версия текста растёт при каждой правке. Факт согласия хранится в users (consent_version, consent_given_at) — второй эшелон проверки на сервере, как и для отключаемых модулей ранее. Дефолт (выключено) сохраняет поведение существующих инсталляций, у уже зарегистрированных пользователей согласие не запрашивается.
136 lines
7.5 KiB
Python
136 lines
7.5 KiB
Python
"""Модели Pydantic для описания `config/plugins.yaml` и его загрузчика."""
|
||
|
||
from pathlib import Path
|
||
from typing import Any, Literal
|
||
|
||
import yaml
|
||
from pydantic import BaseModel, Field
|
||
|
||
|
||
class TranscriberConfig(BaseModel):
|
||
"""Конфигурация активного плагина transcriber."""
|
||
|
||
enabled: bool = True
|
||
provider: str = Field(default="null", min_length=1)
|
||
model: str | None = None
|
||
language: str = "ru"
|
||
options: dict[str, Any] = Field(default_factory=dict)
|
||
|
||
|
||
class SummarizerConfig(BaseModel):
|
||
"""Конфигурация активного плагина summarizer."""
|
||
|
||
enabled: bool = True
|
||
provider: str = Field(default="null", min_length=1)
|
||
model: str | None = None
|
||
chunk_minutes: int = 20
|
||
options: dict[str, Any] = Field(default_factory=dict)
|
||
|
||
|
||
class ChatConfig(BaseModel):
|
||
"""Конфигурация переключателя функции чата."""
|
||
|
||
enabled: bool = True
|
||
|
||
|
||
class HandQueueConfig(BaseModel):
|
||
"""Конфигурация переключателя модуля «поднятие руки» (кнопка + очередь целиком)."""
|
||
|
||
enabled: bool = True
|
||
|
||
|
||
class PluginsConfig(BaseModel):
|
||
"""Корневая модель конфигурации для `config/plugins.yaml`."""
|
||
|
||
transcriber: TranscriberConfig = Field(default_factory=TranscriberConfig)
|
||
summarizer: SummarizerConfig = Field(default_factory=SummarizerConfig)
|
||
chat: ChatConfig = Field(default_factory=ChatConfig)
|
||
hand_queue: HandQueueConfig = Field(default_factory=HandQueueConfig)
|
||
|
||
|
||
def load_plugins_config(path: str | Path) -> PluginsConfig:
|
||
"""Загрузить и валидировать `PluginsConfig` из YAML файла."""
|
||
raw = yaml.safe_load(Path(path).read_text()) or {}
|
||
return PluginsConfig.model_validate(raw)
|
||
|
||
|
||
# --- Настройки инстанса: БД поверх дефолтов `plugins.yaml`. ---
|
||
# Ключи `instance_settings` зеркалят секции ниже (`transcriber`, `summarizer`,
|
||
# `chat`, `ai_level`, `summary_recipients`, `display_timezone`) — см.
|
||
# `services/instance_settings.py`.
|
||
|
||
AiLevel = Literal["min", "medium", "max"]
|
||
"""Уровень AI-модуля инстанса (модели/требования — ADR-004,
|
||
`docs/architecture/adr/004-ai-tier-matrix.md`, `services/ai_tiers.TIERS`).
|
||
Доступность каждого уровня на конкретном инстансе зависит от обнаруженного
|
||
железа и скачанных моделей — см. `services/ai_levels.py::detect_ai_levels`."""
|
||
|
||
SummaryRecipientsMode = Literal["all", "owner"]
|
||
"""Режим рассылки саммари по умолчанию: всем участникам либо только
|
||
владельцу конференции (переопределяется на уровне `conferences.summary_recipients`)."""
|
||
|
||
PublishQualityCap = Literal["off", "180p", "360p", "720p"]
|
||
"""Потолок качества исходящего видео участника (задача «рычаги качества
|
||
медиа»). `off` — без ограничения (дефолт, поведение как до появления
|
||
настройки). Остальные значения режут `publishDefaults.videoEncoding` и
|
||
`videoSimulcastLayers` на клиенте (`frontend/src/lib/publishQualityCap.ts`) —
|
||
именно битрейт верхнего слоя симулкаста, а не жёсткое разрешение захвата
|
||
камеры; фактическое разрешение WebRTC подстраивает под битрейт сам."""
|
||
|
||
StageMaxTiles = Literal[4, 9, 16, 25]
|
||
"""Потолок числа одновременно видимых плиток на сцене (`StageGrid`) —
|
||
режет набор доступных раскладок сетки, что бросает лишних участников на
|
||
следующую страницу пагинации вместо подписки на их треки. `25` — дефолт,
|
||
совпадает с текущим максимумом сетки (5×5), то есть без ограничения."""
|
||
|
||
|
||
class MediaLimitsConfig(BaseModel):
|
||
"""Рычаги нагрузки медиа для администратора инстанса (не логика состояния
|
||
конференции — статичные потолки, применяются на клиенте при входе)."""
|
||
|
||
publish_quality_cap: PublishQualityCap = "off"
|
||
stage_max_tiles: StageMaxTiles = 25
|
||
|
||
|
||
class InstanceConfig(BaseModel):
|
||
"""Эффективная конфигурация инстанса (значения `instance_settings` поверх дефолтов
|
||
`plugins.yaml`, см. `services/instance_settings.py::load_effective_config`)."""
|
||
|
||
transcriber: TranscriberConfig
|
||
summarizer: SummarizerConfig
|
||
chat: ChatConfig
|
||
hand_queue: HandQueueConfig = Field(default_factory=HandQueueConfig)
|
||
ai_level: AiLevel = "min"
|
||
summary_recipients: SummaryRecipientsMode = "all"
|
||
display_timezone: str = "Europe/Moscow"
|
||
# Разрешить выбор команды на форме регистрации (справочник `teams`)
|
||
# — см. `services/instance_settings.py`.
|
||
registration_team_choice: bool = False
|
||
# Верификация регистрирующихся по домену email: при включении
|
||
# `POST /auth/register` принимает только email с доменом из
|
||
# `registration_email_domains` (любым из списка) — см.
|
||
# `services/instance_settings.py`.
|
||
registration_email_domain_enabled: bool = False
|
||
registration_email_domains: list[str] = Field(default_factory=list)
|
||
# Контактный адрес инстанса — подставляется в `Reply-To` исходящих писем
|
||
# (сами письма уходят от `no-reply@`, отвечать на них некуда без этого
|
||
# адреса) — см. `services/instance_settings.py`, `services/email.py`.
|
||
contact_email_enabled: bool = False
|
||
contact_email: str | None = None
|
||
# Потолок качества публикации + максимум плиток сцены — см.
|
||
# `services/instance_settings.py`. Отдаётся участнику ДО входа в
|
||
# LiveKit-комнату (в ответе join, `schemas/conferences.py::JoinOut`), а
|
||
# не только в админке — настройка должна быть на руках у клиента до
|
||
# публикации трека.
|
||
media_limits: MediaLimitsConfig = Field(default_factory=MediaLimitsConfig)
|
||
# Согласие на обработку персональных данных при регистрации: галочка
|
||
# обязательна только при `consent_required=True`, текст/версия — редактируемая
|
||
# администратором настройка (дефолт — типовой шаблон, не юридический документ) —
|
||
# см. `services/instance_settings.py`. `consent_policy_text`/`_version`
|
||
# отдаются публично (`GET /auth/registration-options`) независимо от
|
||
# `consent_required`, чтобы страница регламента была осмысленной и при
|
||
# выключенном модуле.
|
||
consent_required: bool = False
|
||
consent_policy_text: str = ""
|
||
consent_policy_version: int = 1
|