"""Cloudflare 适配器(REST API v4,Bearer Token) API 文档:https://developers.cloudflare.com/api/ 所需配置:{"api_token": "..."}(建议用 API Token,权限含 Zone:Read / Account:Read / Registrar:Read) Cloudflare 无传统 VPS,主要同步托管域名(zones);Workers/R2/Tunnel 子资产留待后续阶段。 域名到期日通过 Registrar API(/accounts/{id}/registrar/domains)补充获取。 """ import httpx from app.adapters.base import BaseAdapter, NormalizedDomain from app.adapters.registry import register @register("cloudflare-api") class CloudflareAdapter(BaseAdapter): required_config = ["api_token"] BASE = "https://api.cloudflare.com/client/v4" def _headers(self) -> dict: return {"Authorization": f"Bearer {self.config.get('api_token', '')}"} def _get(self, path: str) -> dict: with httpx.Client(timeout=30) as client: resp = client.get(self.BASE + path, headers=self._headers()) resp.raise_for_status() data = resp.json() if not data.get("success", True): errors = data.get("errors", []) raise RuntimeError(errors[0].get("message") if errors else "Cloudflare API 返回失败") return data def test_connection(self) -> dict: try: data = self._get("/user/tokens/verify") status = data.get("result", {}).get("status") if status == "active": return {"ok": True, "message": "API Token 有效"} return {"ok": False, "message": f"Token 状态异常:{status}"} except httpx.HTTPStatusError as e: return {"ok": False, "message": f"HTTP {e.response.status_code}:Token 无效或权限不足"} except Exception as e: # noqa: BLE001 return {"ok": False, "message": str(e)} def list_domains(self) -> list: # 先拉取 Registrar 域名到期日映射(domain_name -> expires_at ISO 日期) expiry_map = self._registrar_expiry_map() result = [] page = 1 while True: data = self._get(f"/zones?per_page=50&page={page}") for z in data.get("result", []): name = z.get("name") result.append( NormalizedDomain( external_id=z.get("id"), domain_name=name, registrar="cloudflare", expiry_date=expiry_map.get(name), status="active" if z.get("status") == "active" else (z.get("status") or "unknown"), raw=z, ) ) info = data.get("result_info", {}) if page >= info.get("total_pages", 1): break page += 1 return result def _registrar_expiry_map(self) -> dict: """拉取 Cloudflare Registrar 域名到期日映射 {domain_name: YYYY-MM-DD} 流程:/accounts → 对每个 account 调 /accounts/{id}/registrar/domains。 若 Token 无 Registrar 权限或账号无 Registrar 域名,静默返回空映射。 """ expiry = {} try: accounts = self._get("/accounts?per_page=50").get("result", []) except Exception: # noqa: BLE001 return expiry for acc in accounts: acc_id = acc.get("id") if not acc_id: continue try: domains = self._get(f"/accounts/{acc_id}/registrar/domains").get("result", []) except Exception: # noqa: BLE001 continue for d in domains: # Registrar API 返回的 id 是域名本身(如 example.com) name = d.get("id") or d.get("domain_name") or "" expires_at = d.get("expires_at") if name and expires_at: # expires_at 为 ISO8601 时间戳,截取日期部分 expiry[name] = expires_at[:10] return expiry