from __future__ import annotations from typing import Literal, Optional from fastapi import APIRouter, Request from fastapi.responses import HTMLResponse from pydantic import BaseModel, HttpUrl from app.core.config import get_settings from app.legal_docs import PRIVACY_POLICY_MD, TERMS_OF_USE_MD, choose_content_by_lang, render_as_simple_html router = APIRouter(prefix="/v1/legal", tags=["legal"]) INTERNAL_SENTINEL = "__internal__" # 当前多语言仅支持 EN / TC(繁体) ResolvedLang = Literal["en", "tc"] class LegalLinksResponse(BaseModel): privacyPolicyUrl: HttpUrl termsOfUseUrl: HttpUrl resolvedLang: ResolvedLang def _resolve_lang(accept_language: Optional[str]) -> ResolvedLang: """ 从 Accept-Language 里做一个轻量语言解析。 说明: - 只关心:en / tc(繁体) - 解析失败或缺失:回退 en """ if not accept_language: return "en" s = accept_language.lower() # 目前只支持 EN / TC:只要是中文或显式 tc,都归到 tc if "tc" in s: return "tc" if "zh" in s or "hant" in s or "tw" in s or "hk" in s or "mo" in s: return "tc" return "en" def _pick_urls(lang: ResolvedLang) -> tuple[str, str, ResolvedLang]: settings = get_settings() if lang == "tc": privacy = settings.legal_privacy_url_tc or settings.legal_privacy_url_en terms = settings.legal_terms_url_tc or settings.legal_terms_url_en # 如果未配置 tc 链接: # - 若走内置页面(__internal__),可直接展示 tc 内容,因此 resolved=tc # - 否则回退到 en 链接,因此 resolved=en if settings.legal_privacy_url_tc or settings.legal_terms_url_tc: resolved: ResolvedLang = "tc" elif privacy == INTERNAL_SENTINEL or terms == INTERNAL_SENTINEL: resolved = "tc" else: resolved = "en" return privacy, terms, resolved return settings.legal_privacy_url_en, settings.legal_terms_url_en, "en" def _join_base_url(base_url: str, path: str) -> str: base = (base_url or "").rstrip("/") p = (path or "").strip() if not p.startswith("/"): p = "/" + p return base + p def _normalize_internal_url(request: Request, url: str, internal_path: str) -> str: """ 将内置哨兵值替换为当前服务的可访问绝对 URL。 """ if url != INTERNAL_SENTINEL: return url return _join_base_url(str(request.base_url), internal_path) @router.get("/links", response_model=LegalLinksResponse) async def get_legal_links(request: Request) -> LegalLinksResponse: """ 获取协议链接(隐私协议 / 使用协议)。 - 语言来源:Accept-Language - 默认兜底:en """ accept_language = request.headers.get("accept-language") lang = _resolve_lang(accept_language) privacy, terms, resolved = _pick_urls(lang) privacy = _normalize_internal_url(request, privacy, "/v1/legal/privacy") terms = _normalize_internal_url(request, terms, "/v1/legal/terms") return LegalLinksResponse(privacyPolicyUrl=privacy, termsOfUseUrl=terms, resolvedLang=resolved) @router.get("/privacy", response_class=HTMLResponse) async def get_privacy_policy(request: Request) -> HTMLResponse: """ 内置隐私协议页面(用于未配置外部托管链接时的兜底)。 """ accept_language = request.headers.get("accept-language") lang = _resolve_lang(accept_language) content, resolved = choose_content_by_lang(PRIVACY_POLICY_MD, lang) title = "Dear Mama | Privacy Policy" if resolved == "en" else "Dear Mama|隱私權政策" page = render_as_simple_html(title=title, content=content) return HTMLResponse(content=page, headers={"Content-Language": "en" if resolved == "en" else "zh-Hant"}) @router.get("/terms", response_class=HTMLResponse) async def get_terms_of_use(request: Request) -> HTMLResponse: """ 内置使用协议页面(用于未配置外部托管链接时的兜底)。 """ accept_language = request.headers.get("accept-language") lang = _resolve_lang(accept_language) content, resolved = choose_content_by_lang(TERMS_OF_USE_MD, lang) title = "Dear Mama – Terms of Use" if resolved == "en" else "Dear Mama 使用條款" page = render_as_simple_html(title=title, content=content) return HTMLResponse(content=page, headers={"Content-Language": "en" if resolved == "en" else "zh-Hant"})