"""Zeabur 适配器(GraphQL Public API,Bearer Token) API 文档:https://zeabur.com/docs/zh-CN/developer/public-api 端点:https://api.zeabur.com/graphql 所需配置:{"api_token": "..."} 覆盖:团队/个人名下的服务器清单、重启服务器。 服务器列表返回 CPU(毫核)、内存/磁盘(MB)与在线状态; Zeabur 的计费走用量结算,没有统一的「到期日/余额」查询,故不声明 get_account,也不提供续费。 """ import httpx from app.adapters.base import BaseAdapter, NormalizedVPS from app.adapters.registry import register _SERVERS_QUERY = """ query Servers { servers { _id name ip country city provisioningStatus providerInfo { code name } hasK3s status { isOnline totalCPU totalMemory totalDisk vmStatus } } } """ @register("zeabur-api") class ZeaburAdapter(BaseAdapter): required_config = ["api_token"] BASE = "https://api.zeabur.com/graphql" def _graphql(self, query: str, variables: dict | None = None) -> dict: payload = {"query": query} if variables: payload["variables"] = variables with httpx.Client(timeout=30) as client: resp = client.post( self.BASE, headers={"Authorization": f"Bearer {self.config.get('api_token', '')}"}, json=payload, ) resp.raise_for_status() data = resp.json() errors = data.get("errors") or [] if errors: message = errors[0].get("message") if isinstance(errors[0], dict) else str(errors[0]) raise RuntimeError(f"Zeabur GraphQL 错误:{message}") return data.get("data") or {} def test_connection(self) -> dict: try: data = self._graphql("query Me { me { username } }") username = (data.get("me") or {}).get("username") or "未知账户" return {"ok": True, "message": f"连接成功,令牌属于 {username}"} except httpx.HTTPStatusError as e: return {"ok": False, "message": f"HTTP {e.response.status_code}:API Token 无效或权限不足"} except Exception as e: # noqa: BLE001 return {"ok": False, "message": str(e)} def list_vps(self) -> list: servers = self._graphql(_SERVERS_QUERY).get("servers") or [] result = [] for server in servers: status = server.get("status") or {} online = bool(status.get("isOnline")) provider = (server.get("providerInfo") or {}).get("name") location = ", ".join( part for part in (server.get("city"), server.get("country")) if part ) total_cpu = int(status.get("totalCPU") or 0) result.append( NormalizedVPS( external_id=str(server.get("_id")), name=server.get("name") or server.get("ip") or str(server.get("_id")), ip_address=server.get("ip") or None, region=location or provider, # hasK3s==False 才是纯净 Ubuntu;nil 是旧机器,默认装了 ZeaburOS os="Ubuntu" if server.get("hasK3s") is False else "ZeaburOS", # totalCPU 单位是毫核(4000 = 4 核),台账列是整数核 cpu_cores=max(1, round(total_cpu / 1000)) if total_cpu else None, memory_gb=round(int(status.get("totalMemory") or 0) / 1024, 1) or None, disk_gb=round(int(status.get("totalDisk") or 0) / 1024) or None, status="active" if online else "stopped", currency="USD", charge_type="postpaid", raw=server, ) ) return result def reboot_instance(self, external_id: str) -> dict: mutation = """ mutation RebootServer($id: ObjectID!) { rebootServer(_id: $id) } """ try: data = self._graphql(mutation, {"id": external_id}) if not data.get("rebootServer"): return {"ok": False, "message": "重启请求未被接受"} return {"ok": True, "message": "重启指令已下发"} except httpx.HTTPStatusError as e: return {"ok": False, "message": f"HTTP {e.response.status_code}:重启失败"} except Exception as e: # noqa: BLE001 return {"ok": False, "message": f"重启失败:{e}"}