"""Browser-reachable addresses written into generated HTML pages. The settings dialog is the source of truth. A value saved in ``system_settings.json`` wins over ``config.yaml`` and the built-in default, so a deployment can point new pages at a domain users can actually open without editing or restarting from a config file. """ from __future__ import annotations import logging from urllib.parse import urlparse from deerflow.config.html_runtime_config import ( DEFAULT_HTML_MAP_TILE_URL, DEFAULT_HTML_RUNTIME_BASE_URL, ) logger = logging.getLogger(__name__) _HTML_RUNTIME_SUFFIX = "/api/public/html-runtime" def normalize_html_runtime_base_url(value: str) -> str: """Return an absolute runtime base, appending the public asset path when omitted.""" raw = str(value or "").strip() if not raw: raise ValueError("请填写用户浏览器能够访问的 HTML 运行库地址") if any(character.isspace() for character in raw): raise ValueError("HTML 运行库地址不能包含空格") parsed = urlparse(raw) if parsed.scheme not in {"http", "https"} or not parsed.netloc: raise ValueError("HTML 运行库地址需要以 http:// 或 https:// 开头,并包含域名或 IP") if parsed.username or parsed.password: raise ValueError("HTML 运行库地址不能包含账号或密码") if parsed.query or parsed.fragment: raise ValueError("HTML 运行库地址不需要查询参数") path = parsed.path.rstrip("/") if path in {"", "/"}: path = _HTML_RUNTIME_SUFFIX return f"{parsed.scheme}://{parsed.netloc}{path}" def normalize_html_map_tile_url(value: str) -> str: """Return a Leaflet tile template containing the x/y/z placeholders.""" raw = str(value or "").strip() if not raw: raise ValueError("请填写地图瓦片地址") if any(character.isspace() for character in raw): raise ValueError("地图瓦片地址不能包含空格") parsed = urlparse(raw) if parsed.scheme not in {"http", "https"} or not parsed.netloc: raise ValueError("地图瓦片地址需要以 http:// 或 https:// 开头,并包含域名或 IP") if parsed.username or parsed.password: raise ValueError("地图瓦片地址不能包含账号或密码") lowered = raw.lower() missing = [token for token in ("{z}", "{x}", "{y}") if token not in lowered] if missing: raise ValueError("地图瓦片地址需要包含 {z}、{x}、{y} 三个占位符") return raw def html_runtime_base_url(fallback: str | None = None) -> str: """Return the runtime base used when writing or serving generated HTML.""" saved = _saved("base_url") if saved: try: return normalize_html_runtime_base_url(saved) except ValueError: logger.warning("Ignoring invalid saved HTML runtime base URL") return _first_valid_base(fallback, _config_value("base_url"), DEFAULT_HTML_RUNTIME_BASE_URL) def html_map_tile_url(fallback: str | None = None) -> str: """Return the Leaflet tile template injected into deerflow-runtime.js.""" saved = _saved("map_tile_url") if saved: try: return normalize_html_map_tile_url(saved) except ValueError: logger.warning("Ignoring invalid saved HTML map tile URL") return _first_valid_tile(fallback, _config_value("map_tile_url"), DEFAULT_HTML_MAP_TILE_URL) def _saved(field: str) -> str: try: from deerflow.config.system_settings import load_system_settings block = load_system_settings().html_runtime return str(getattr(block, field, "") or "").strip() except Exception: logger.debug("HTML runtime settings are unavailable", exc_info=True) return "" def _config_value(field: str) -> str: try: from deerflow.config import get_app_config runtime = get_app_config().html_runtime return str(getattr(runtime, field, "") or "").strip() except Exception: logger.debug("HTML runtime config fallback is unavailable", exc_info=True) return "" def _first_valid_base(*candidates: str | None) -> str: for candidate in candidates: if not candidate or not str(candidate).strip(): continue try: return normalize_html_runtime_base_url(str(candidate)) except ValueError: continue return DEFAULT_HTML_RUNTIME_BASE_URL def _first_valid_tile(*candidates: str | None) -> str: for candidate in candidates: if not candidate or not str(candidate).strip(): continue try: return normalize_html_map_tile_url(str(candidate)) except ValueError: continue return DEFAULT_HTML_MAP_TILE_URL