线上平台列表里 cloudcone、zeabur、xinwang、dynadot、agnes 的 sdk_type 仍为空, 平台卡片只能显示「未对接」,用户无法同步。按各自真实 API 能力补齐: - CloudCone:实例清单 + 开机/关机/重启;无公开账单接口,不声明余额与续费 - Zeabur:服务器清单 + 重启;毫核/MB 归一化为核/GB,未采集时不伪造规格 - Dynadot:域名清单(含到期日)+ 账户余额;set_dns2 位置参数无法安全 CRUD - 新网:代理 API 按域名逐个查到期日;无列表接口,需在账号里填 domains 清单 - Agnes:/models 校验 Key;无余额接口,如实标记不支持 配套改动: - _align_preset_providers 为库中为空的 sdk_type 回填种子值(启动即生效), 避免新适配器上线后线上仍停留在「未对接」 - 适配器可声明 plain_config,代理号/域名等非密钥字段前端按明文输入 - PRESET_PROVIDERS 五条记录填上对应 sdk_type 测试:445 passed(新增 tests/test_platform_adapters.py),vitest 62 passed。
108 lines
4.1 KiB
Python
108 lines
4.1 KiB
Python
"""CloudCone 适配器(REST API v2)
|
||
|
||
API 文档:https://cloudcone-api.readme.io/reference
|
||
所需配置:{"api_key": "...", "api_hash": "..."}
|
||
认证请求头:App-Secret = API Key,Hash = API Hash
|
||
|
||
覆盖:实例清单、开机/关机/重启。
|
||
CloudCone 没有公开的账单/到期/余额接口,故不声明 get_account,
|
||
也不提供续费动作——拿不到到期日就别假装能核对。
|
||
"""
|
||
|
||
import httpx
|
||
|
||
from app.adapters.base import BaseAdapter, NormalizedVPS
|
||
from app.adapters.registry import register
|
||
|
||
#: CloudCone state.name -> 台账状态
|
||
_STATUS_MAP = {
|
||
"active": "active",
|
||
"starting": "active",
|
||
"rebooting": "active",
|
||
"stopped": "stopped",
|
||
"shutdown": "stopped",
|
||
"suspended": "stopped",
|
||
}
|
||
|
||
|
||
@register("cloudcone-api")
|
||
class CloudConeAdapter(BaseAdapter):
|
||
required_config = ["api_key", "api_hash"]
|
||
BASE = "https://api.cloudcone.com/api/v2"
|
||
|
||
def _headers(self) -> dict:
|
||
return {
|
||
"App-Secret": self.config.get("api_key", ""),
|
||
"Hash": self.config.get("api_hash", ""),
|
||
}
|
||
|
||
def _request(self, method: str, path: str) -> dict:
|
||
with httpx.Client(timeout=30) as client:
|
||
resp = client.request(method, self.BASE + path, headers=self._headers())
|
||
resp.raise_for_status()
|
||
data = resp.json() if resp.content else {}
|
||
# CloudCone 用 status=1 表示成功,0 时 message 里才有可读原因
|
||
if isinstance(data, dict) and data.get("status") not in (1, "1", None):
|
||
raise RuntimeError(data.get("message") or "CloudCone 接口返回失败")
|
||
return data
|
||
|
||
def _get(self, path: str) -> dict:
|
||
return self._request("GET", path)
|
||
|
||
def _post(self, path: str) -> dict:
|
||
return self._request("POST", path)
|
||
|
||
def test_connection(self) -> dict:
|
||
try:
|
||
instances = self.list_vps()
|
||
return {"ok": True, "message": f"连接成功,读取到 {len(instances)} 台实例"}
|
||
except httpx.HTTPStatusError as e:
|
||
return {"ok": False, "message": f"HTTP {e.response.status_code}:API Key/Hash 无效或权限不足"}
|
||
except Exception as e: # noqa: BLE001
|
||
return {"ok": False, "message": str(e)}
|
||
|
||
def list_vps(self) -> list:
|
||
data = self._get("/compute/instances")
|
||
raw_instances = (data.get("__data") or {}).get("instances") or []
|
||
result = []
|
||
for inst in raw_instances:
|
||
state = (inst.get("state") or {}).get("name") or "unknown"
|
||
ips = str(inst.get("ips") or "").strip()
|
||
result.append(
|
||
NormalizedVPS(
|
||
external_id=str(inst.get("id")),
|
||
name=inst.get("hostname") or inst.get("nodename") or str(inst.get("id")),
|
||
ip_address=ips.split(",")[0].strip() if ips else None,
|
||
region=inst.get("nodename"),
|
||
os=inst.get("distro"),
|
||
cpu_cores=inst.get("cpu"),
|
||
memory_gb=inst.get("ram"),
|
||
disk_gb=inst.get("disk"),
|
||
status=_STATUS_MAP.get(state.lower(), "unknown"),
|
||
currency="USD",
|
||
charge_type="prepaid",
|
||
raw=inst,
|
||
)
|
||
)
|
||
return result
|
||
|
||
# ---------------- 实例管理动作 ----------------
|
||
|
||
def _instance_action(self, external_id: str, verb: str, label: str) -> dict:
|
||
try:
|
||
self._post(f"/compute/{external_id}/{verb}")
|
||
return {"ok": True, "message": f"{label}指令已下发"}
|
||
except httpx.HTTPStatusError as e:
|
||
return {"ok": False, "message": f"HTTP {e.response.status_code}:{label}失败"}
|
||
except Exception as e: # noqa: BLE001
|
||
return {"ok": False, "message": f"{label}失败:{e}"}
|
||
|
||
def start_instance(self, external_id: str) -> dict:
|
||
return self._instance_action(external_id, "boot", "开机")
|
||
|
||
def stop_instance(self, external_id: str) -> dict:
|
||
return self._instance_action(external_id, "shutdown", "关机")
|
||
|
||
def reboot_instance(self, external_id: str) -> dict:
|
||
return self._instance_action(external_id, "reboot", "重启")
|