"""Автонастройка из окружения и внутренней сети Docker. Пока администратор сам не заполнил поле в настройках, значение берётся: 1) из переменной окружения (docker-compose.yml передаёт их из .env), иначе 2) из внутренней сети Docker: если рядом запущен сервис с известным именем (nextcloud, onlyoffice, ollama — профили docker-compose.yml), подставляется его внутренний адрес. Значение, сохранённое в интерфейсе, всегда важнее. Имена сервисов проверяются через DNS Docker с кэшем на минуту.""" import ipaddress import os import socket import time from urllib.parse import urlsplit CACHE_SEC = 60 _seen: dict[str, tuple[float, bool]] = {} # настройка -> переменная окружения ENV = { "nc_url": "NC_URL", "nc_user": "NC_USER", "nc_password": "NC_PASSWORD", "nc_root": "NC_ROOT", "onlyoffice_url": "ONLYOFFICE_URL", "onlyoffice_jwt": "ONLYOFFICE_JWT", "portal_internal_url": "PORTAL_INTERNAL_URL", "assistant_provider": "ASSISTANT_PROVIDER", "assistant_base_url": "ASSISTANT_BASE_URL", "assistant_model": "ASSISTANT_MODEL", "assistant_api_key": "ASSISTANT_API_KEY", } # настройка -> (имя сервиса в сети Docker, адрес, который подставить) SERVICES = { "nc_url": ("nextcloud", "http://nextcloud"), "onlyoffice_url": ("onlyoffice", "http://onlyoffice"), "assistant_base_url": ("ollama", "http://ollama:11434/v1"), } def resolves(host: str) -> bool: now = time.monotonic() hit = _seen.get(host) if hit and now - hit[0] < CACHE_SEC: return hit[1] try: socket.getaddrinfo(host, None) ok = True except OSError: ok = False _seen[host] = (now, ok) return ok def default(key: str) -> str | None: """Значение по умолчанию из окружения или сети Docker; None — автонастройки для ключа нет.""" env = ENV.get(key) if env and os.getenv(env): return os.getenv(env, "").strip() svc = SERVICES.get(key) if svc and resolves(svc[0]): return svc[1] if key == "assistant_model" and resolves("ollama"): return os.getenv("OLLAMA_MODEL", "qwen2.5:7b") if key == "portal_internal_url" and resolves("onlyoffice"): return "http://app:8000" # по этому адресу ONLYOFFICE забирает документы у портала return None def is_local(url: str) -> bool: """Адрес модели внутри своей сети (ключ API не нужен): localhost, имя сервиса Docker без точки, частный IP.""" host = (urlsplit(url or "").hostname or "").lower() if not host: return False if host in ("localhost", "127.0.0.1", "::1") or "." not in host: return True try: return ipaddress.ip_address(host).is_private except ValueError: return False