"""OSS 转存:把帖子媒体直链转存到公网 CDN,返回 cdn_url(绕过防盗链 / 临时签名链时效)。 接口(实测 2026-06-26): POST settings.oss_upload_url body{src_url, src_type:"video"|"image"} → {status:0, oss_object:{cdn_url:"https://res.cybertogether.net/...", oss_object_key, ...}} 纯 parser(parse_oss_response)可离线测;upload_stream 负责 HTTP(失败抛 OssError); to_oss 是兜底包装——任何失败返回原 src_url、永不抛,供 decompose 降级不阻断。 """ from __future__ import annotations from typing import Any, Callable, Optional import httpx from core.config import Settings class OssError(RuntimeError): pass def parse_oss_response(response: Any) -> str: """{status:0, oss_object:{cdn_url}} → cdn_url。status!=0 或缺 cdn_url 抛 OssError。""" if not isinstance(response, dict): raise OssError("bad_response: not a dict") status = response.get("status") if status not in (0, "0"): raise OssError(f"business_error: status={status} msg={response.get('msg')}") obj = response.get("oss_object") or {} cdn = obj.get("cdn_url") if not cdn: raise OssError("empty_cdn_url: oss_object.cdn_url missing") return cdn def upload_stream( src_url: str, *, src_type: str, settings: Optional[Settings] = None, http_post: Callable[..., Any] = httpx.post, env_file: str = ".env", ) -> str: """转存一条直链到 OSS,返回 cdn_url;HTTP/解析失败抛 OssError。""" settings = settings or Settings.from_env(env_file) if src_type not in ("video", "image"): raise OssError(f"bad_src_type: {src_type}") try: resp = http_post( settings.oss_upload_url, json={"src_url": src_url, "src_type": src_type}, headers={"Content-Type": "application/json"}, timeout=settings.oss_upload_timeout_seconds, ) resp.raise_for_status() data = resp.json() except httpx.HTTPError as exc: raise OssError(f"http_error: {exc}") from exc except ValueError as exc: raise OssError("bad_json") from exc return parse_oss_response(data) def to_oss( src_url: str, src_type: str, *, settings: Optional[Settings] = None, http_post: Callable[..., Any] = httpx.post, env_file: str = ".env", ) -> str: """兜底包装:成功返回 cdn_url,任何失败(含空入参)返回原 src_url,永不抛。""" if not src_url: return src_url try: return upload_stream(src_url, src_type=src_type, settings=settings, http_post=http_post, env_file=env_file) except Exception: return src_url