"""Server-side Docs metadata and Gatekeeper authorization helpers.""" from __future__ import annotations from dataclasses import dataclass from pathlib import Path from typing import Literal from app.models.user import User DocsAccess = Literal["public", "docs_user", "docs_developer", "docs_admin"] DocsLang = Literal["zh", "en"] VALID_DOCS_LANGS = {"zh", "en"} DOCS_README_FILENAME = "README.md" DEFAULT_DOCS_SLUG = "overview" REPO_ROOT = Path(__file__).resolve().parents[3] TECHNICAL_DOCS_ROOT = REPO_ROOT / "docs" / "technical" @dataclass(frozen=True) class DocsMetadata: filename: str slug: str access: DocsAccess group: str order: int zh_title: str en_title: str DOCS_METADATA: tuple[DocsMetadata, ...] = ( DocsMetadata(DOCS_README_FILENAME, DEFAULT_DOCS_SLUG, "public", "Overview", 0, "技术文档", "Technical Docs"), DocsMetadata("quickstart.md", "quickstart", "public", "Manual", 1, "快速开始", "Quickstart"), DocsMetadata("manual.md", "manual", "public", "Manual", 2, "Planet 使用手册", "Planet Manual"), DocsMetadata("location-pipeline-user.md", "location-pipeline-user", "public", "Manual", 3, "Earth 位置候选采集使用手册", "Earth Location Candidate Collection User Guide"), DocsMetadata("earth-frontend-context.md", "earth-frontend-context", "docs_developer", "Earth", 10, "Earth 前端结构", "Earth Frontend Context"), DocsMetadata("earth-layer-style-reference.md", "earth-layer-style-reference", "docs_developer", "Earth", 11, "Earth 图层样式属性索引", "Earth Layer Style Reference"), DocsMetadata("earth-render-layer-order.md", "earth-render-layer-order", "docs_developer", "Earth", 12, "Earth 渲染图层顺序", "Earth Render Layer Order"), DocsMetadata("earth-satellite-footprint-policy.md", "earth-satellite-footprint-policy", "docs_developer", "Earth", 13, "Earth 卫星覆盖策略", "Earth Satellite Footprint Policy"), DocsMetadata("earth-bgp-context.md", "earth-bgp-context", "docs_developer", "Earth", 14, "BGP 态势上下文", "BGP Context"), DocsMetadata("earth-news-live-streams-collector-format.md", "earth-news-live-streams-collector-format", "docs_developer", "Earth", 15, "新闻直播采集格式", "News Live Streams Collector Format"), DocsMetadata("earth-interactable-usage.md", "earth-interactable-usage", "docs_developer", "Earth", 16, "Earth 可交互图标接入", "Earth Interactable Usage"), DocsMetadata("earth-toolbar-overlay-coordination.md", "earth-toolbar-overlay-coordination", "docs_developer", "Earth", 17, "Earth 工具栏与浮层协同", "Earth Toolbar and Overlay Coordination"), DocsMetadata("frontend-admin-frontend-context.md", "frontend-admin-frontend-context", "docs_developer", "Frontend", 20, "控制台前端结构", "Admin Frontend Context"), DocsMetadata("frontend-layout-guidelines.md", "frontend-layout-guidelines", "docs_developer", "Frontend", 21, "前端布局指南", "Frontend Layout Guidelines"), DocsMetadata("docs-gatekeeper-development.md", "docs-gatekeeper-development", "docs_developer", "Frontend", 22, "Docs Gatekeeper 开发说明", "Docs Gatekeeper Development Guide"), DocsMetadata("backend-collectors.md", "backend-collectors", "docs_developer", "Backend", 30, "数据采集系统", "Data Collectors"), DocsMetadata("backend-system-service-control.md", "backend-system-service-control", "docs_admin", "Backend", 31, "系统服务控制", "System Service Control"), DocsMetadata("datasource-collector-settings-connectivity.md", "datasource-collector-settings-connectivity", "docs_developer", "Backend", 32, "数据源、采集器设置与连接验证", "Datasource Collector Settings and Connectivity"), DocsMetadata("backend-datasources-api-performance.md", "backend-datasources-api-performance", "docs_developer", "Backend", 33, "数据源 API 性能", "Datasource API Performance"), DocsMetadata("location-pipeline-development.md", "location-pipeline-development", "docs_developer", "Backend", 34, "通用位置估算管线开发说明", "Shared Location Resolution Pipeline Development Guide"), DocsMetadata("agents-aiprovider.md", "agents-aiprovider", "docs_developer", "Agents", 40, "AI Provider 指南", "AI Provider Guide"), DocsMetadata("ops-docker-compose-buildx-upgrade.md", "ops-docker-compose-buildx-upgrade", "docs_admin", "Ops", 50, "Docker + Compose + Buildx 升级", "Docker + Compose + Buildx Upgrade"), DocsMetadata("ops-planet-sh-startup.md", "ops-planet-sh-startup", "docs_admin", "Ops", 51, "planet.sh 启动机制", "planet.sh Startup"), ) DOCS_BY_SLUG = {entry.slug: entry for entry in DOCS_METADATA} def get_user_gatekeeper_groups(user: User | None) -> set[str]: if user is None: return set() role = user.role.value if hasattr(user.role, "value") else str(user.role or "") if role == "super_admin": return {"docs_user", "docs_developer", "docs_admin"} if role == "admin": return {"docs_user", "docs_developer", "docs_admin"} groups = set() raw_groups = user.gatekeeper_groups or [] if isinstance(raw_groups, list): groups.update(str(group) for group in raw_groups) if "docs_admin" in groups: groups.update({"docs_developer", "docs_user"}) if "docs_developer" in groups: groups.add("docs_user") return groups def can_read_doc(entry: DocsMetadata, user: User | None) -> bool: if entry.access == "public": return True return entry.access in get_user_gatekeeper_groups(user) def doc_path_for(entry: DocsMetadata, lang: str) -> Path: if lang not in VALID_DOCS_LANGS: raise ValueError("Unsupported docs language") return TECHNICAL_DOCS_ROOT / lang / entry.filename def title_for(entry: DocsMetadata, lang: str) -> str: return entry.zh_title if lang == "zh" else entry.en_title def catalog_for_user(user: User | None) -> list[dict]: items: list[dict] = [] for entry in DOCS_METADATA: if not can_read_doc(entry, user): continue for lang in sorted(VALID_DOCS_LANGS): if not doc_path_for(entry, lang).exists(): continue items.append( { "slug": entry.slug, "filename": entry.filename, "lang": lang, "title": title_for(entry, lang), "group": entry.group, "order": entry.order, "access": entry.access, } ) return sorted(items, key=lambda item: (item["lang"], item["order"], item["title"]))