# CLI 与 SDK 指南

## 概览

Clore.ai 提供了一个 **REST API** 使得可以对 GPU 市场进行完整的编程访问 — 列出服务器、创建订单、监控部署和取消租用。

> **注意：** 目前没有官方的 CLI 二进制。所有自动化均通过 REST API 直接完成，使用诸如 `curl`、Python 或 Node.js 等工具。

**基础 URL：** `https://api.clore.ai/v1`

**响应格式：** JSON。每个响应都包含一个 `code` 字段指示状态。

***

## 身份验证

从以下位置生成您的 API 密钥： [Clore.ai 仪表板](https://clore.ai):

1. 登录到您的帐户
2. 导航到 **API** 设置中的部分
3. 生成并复制您的 API 密钥

**头部格式：**

```
auth: YOUR_API_KEY
```

> ⚠️ **重要：** auth 头是 `auth`, **不是** `Authorization: Bearer`。使用错误的格式会返回代码 `3` （无效的 API 令牌）。

**示例：**

```bash
curl -H 'auth: YOUR_API_KEY' 'https://api.clore.ai/v1/marketplace'
```

***

## 快速开始

### 列出市场服务器

浏览所有可用的 GPU 服务器：

```bash
curl -XGET \
  -H 'auth: YOUR_API_KEY' \
  'https://api.clore.ai/v1/marketplace'
```

**响应：**

```json
{
  "servers": [
    {
      "id": 6,
      "owner": 4,
      "mrl": 73,
      "price": {
        "on_demand": { "bitcoin": 0.00001 },
        "spot": { "bitcoin": 0.000001 }
      },
      "rented": false,
      "specs": {
        "cpu": "Intel Core i9-11900",
        "ram": 62.67,
        "gpu": "1x NVIDIA GeForce GTX 1080 Ti",
        "gpuram": 11,
        "net": { "up": 26.38, "down": 118.42, "cc": "CZ" }
      }
    }
  ],
  "my_servers": [1, 2, 4],
  "code": 0
}
```

***

### 获取您的订单

```bash
curl -XGET \
  -H 'auth: YOUR_API_KEY' \
  'https://api.clore.ai/v1/my_orders'
```

是否包含已完成/已过期的订单：

```bash
curl -XGET \
  -H 'auth: YOUR_API_KEY' \
  'https://api.clore.ai/v1/my_orders?return_completed=true'
```

***

### 创建订单（按需）

```bash
curl -XPOST \
  -H 'auth: YOUR_API_KEY' \
  -H 'Content-type: application/json' \
  -d '{
    "currency": "bitcoin",
    "image": "cloreai/ubuntu20.04-jupyter",
    "renting_server": 6,
    "type": "on-demand",
    "ports": {
      "22": "tcp",
      "8888": "http"
    },
    "ssh_password": "YourSSHPassword123",
    "jupyter_token": "YourJupyterToken123"
  }' \
  'https://api.clore.ai/v1/create_order'
```

**响应：**

```json
{ "code": 0 }
```

***

### 创建一个竞价（Spot）订单

竞价订单更便宜但可能被超价。您设定每天的价格：

```bash
curl -XPOST \
  -H 'auth: YOUR_API_KEY' \
  -H 'Content-type: application/json' \
  -d '{
    "currency": "bitcoin",
    "image": "cloreai/ubuntu20.04-jupyter",
    "renting_server": 6,
    "type": "spot",
    "spotprice": 0.000005,
    "ports": {
      "22": "tcp",
      "8888": "http"
    },
    "ssh_password": "YourSSHPassword123"
  }' \
  'https://api.clore.ai/v1/create_order'
```

***

### 检查订单状态

```bash
curl -XGET \
  -H 'auth: YOUR_API_KEY' \
  'https://api.clore.ai/v1/my_orders'
```

活动订单包含 `pub_cluster` （主机名）以及 `tcp_ports` 用于 SSH 访问：

```json
{
  "id": 38,
  "pub_cluster": ["n1.c1.clorecloud.net", "n2.c1.clorecloud.net"],
  "tcp_ports": ["22:10000"],
  "http_port": "8888",
  "expired": false
}
```

通过 SSH 登录到您租用的服务器：

```bash
ssh root@n1.c1.clorecloud.net -p 10000
```

***

### 取消订单

```bash
curl -XPOST \
  -H 'auth: YOUR_API_KEY' \
  -H 'Content-type: application/json' \
  -d '{
    "id": 38
  }' \
  'https://api.clore.ai/v1/cancel_order'
```

可选地报告服务器问题：

```bash
curl -XPOST \
  -H 'auth: YOUR_API_KEY' \
  -H 'Content-type: application/json' \
  -d '{
    "id": 38,
    "issue": "GPU was overheating and throttling performance"
  }' \
  'https://api.clore.ai/v1/cancel_order'
```

***

## Python SDK

一个使用 `requests` 库的轻量封装。使用以下命令安装：

```bash
pip install requests
```

### CloreClient 类

```python
import requests
import time

class CloreClient:
    """Clore.ai REST API 的简单 Python SDK。"""
    
    BASE_URL = "https://api.clore.ai/v1"
    
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.session = requests.Session()
        self.session.headers.update({"auth": api_key})
    
    def _get(self, endpoint: str, params: dict = None) -> dict:
        url = f"{self.BASE_URL}/{endpoint}"
        response = self.session.get(url, params=params)
        response.raise_for_status()
        data = response.json()
        if data.get("code") != 0:
            raise CloreAPIError(data)
        return data
    
    def _post(self, endpoint: str, body: dict) -> dict:
        url = f"{self.BASE_URL}/{endpoint}"
        self.session.headers.update({"Content-type": "application/json"})
        response = self.session.post(url, json=body)
        response.raise_for_status()
        data = response.json()
        if data.get("code") != 0:
            raise CloreAPIError(data)
        return data
    
    def list_servers(self) -> list:
        """获取市场上所有可用的服务器。"""
        data = self._get("marketplace")
        return data["servers"]
    
    def get_server_details(self, server_id: int) -> dict:
        """获取特定服务器的竞价市场信息。"""
        data = self._get("spot_marketplace", params={"market": server_id})
        return data["market"]
    
    def create_order(
        self,
        server_id: int,
        image: str,
        order_type: str = "on-demand",
        currency: str = "bitcoin",
        spotprice: float = None,
        ports: dict = None,
        ssh_password: str = None,
        ssh_key: str = None,
        jupyter_token: str = None,
        env: dict = None,
        command: str = None,
    ) -> dict:
        """
        创建按需或竞价订单。
        
        参数：
            server_id：要租用的服务器 ID
            image：Docker 镜像（例如 'cloreai/ubuntu20.04-jupyter'）
            order_type：'on-demand' 或 'spot'
            currency：'bitcoin'（默认）
            spotprice：竞价订单必需 — 每天的 BTC 价格
            ports：端口转发，例如 {"22": "tcp", "8888": "http"}
            ssh_password：SSH 密码（仅字母数字字符）
            ssh_key：SSH 公钥
            jupyter_token：Jupyter 笔记本令牌
            env：环境变量字典
            command：容器启动后要运行的 shell 命令
        """
        if order_type == "spot" and spotprice is None:
            raise ValueError("spotprice is required for spot orders")
        
        body = {
            "currency": currency,
            "image": image,
            "renting_server": server_id,
            "type": order_type,
        }
        
        if spotprice is not None:
            body["spotprice"] = spotprice
        if ports:
            body["ports"] = ports
        if ssh_password:
            body["ssh_password"] = ssh_password
        if ssh_key:
            body["ssh_key"] = ssh_key
        if jupyter_token:
            body["jupyter_token"] = jupyter_token
        if env:
            body["env"] = env
        if command:
            body["command"] = command
        
        return self._post("create_order", body)
    
    def get_orders(self, include_completed: bool = False) -> list:
        """获取您的活动（可选包括已完成）订单。"""
        params = {"return_completed": "true"} if include_completed else {}
        data = self._get("my_orders", params=params)
        return data["orders"]
    
    def cancel_order(self, order_id: int, issue: str = None) -> dict:
        """取消订单。可选地报告问题。"""
        body = {"id": order_id}
        if issue:
            body["issue"] = issue
        return self._post("cancel_order", body)
    
    def get_wallets(self) -> list:
        """获取您的钱包和余额。"""
        data = self._get("wallets")
        return data["wallets"]
    
    def get_my_servers(self) -> list:
        """获取您提供给市场的服务器。"""
        data = self._get("my_servers")
        return data["servers"]


class CloreAPIError(Exception):
    """当 Clore API 返回非零代码时抛出。"""
    
    ERROR_CODES = {
        0: "正常",
        1: "数据库错误",
        2: "无效的输入数据",
        3: "无效的 API 令牌",
        4: "无效的端点",
        5: "超过速率限制（1 次/秒）",
        6: "错误（见 error 字段）",
    }
    
    def __init__(self, response: dict):
        self.code = response.get("code")
        self.error = response.get("error", "")
        message = self.ERROR_CODES.get(self.code, f"Unknown code {self.code}")
        if self.error:
            message = f"{message}: {self.error}"
        super().__init__(f"Clore API error {self.code}: {message}")
```

### 完整可运行示例

```python
import os
from clore_client import CloreClient, CloreAPIError

# 初始化客户端
client = CloreClient(api_key=os.environ["CLORE_API_KEY"])

# 1. 浏览市场
servers = client.list_servers()
print(f"在市场上找到 {len(servers)} 台服务器")

# 2. 过滤可用的 RTX 4090 服务器
rtx4090_servers = [
    s for s in servers
    if "4090" in s["specs"].get("gpu", "") and not s["rented"]
]

if not rtx4090_servers:
    print("未找到可用的 RTX 4090 服务器")
    exit(1)

# 3. 选择最便宜的一台
cheapest = min(rtx4090_servers, key=lambda s: s["price"]["on_demand"]["bitcoin"])
print(f"最便宜的 RTX 4090：服务器 ID {cheapest['id']}， "
      f"价格 {cheapest['price']['on_demand']['bitcoin']:.8f} BTC/天")

# 4. 创建订单
try:
    client.create_order(
        server_id=cheapest["id"],
        image="cloreai/ubuntu20.04-jupyter",
        order_type="on-demand",
        currency="bitcoin",
        ports={"22": "tcp", "8888": "http"},
        ssh_password="SecurePass123",
        jupyter_token="MyToken123",
    )
    print("订单创建成功！")
except CloreAPIError as e:
    print(f"创建订单失败：{e}")
    exit(1)

# 5. 检查您的订单
orders = client.get_orders()
for order in orders:
    if not order.get("expired"):
        cluster = order.get("pub_cluster", [])
        tcp = order.get("tcp_ports", [])
        print(f"订单 {order['id']}: 服务器 {order['si']}")
        if cluster and tcp:
            ssh_port = tcp[0].split(":")[1]
            print(f"  SSH: ssh root@{cluster[0]} -p {ssh_port}")
```

***

## Node.js 示例

使用原生 `fetch` API（Node.js 18+）：

```javascript
const BASE_URL = 'https://api.clore.ai/v1';

class CloreClient {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.defaultHeaders = {
      'auth': apiKey,
    };
  }

  async get(endpoint, params = {}) {
    const url = new URL(`${BASE_URL}/${endpoint}`);
    Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
    
    const res = await fetch(url.toString(), {
      headers: this.defaultHeaders,
    });
    
    const data = await res.json();
    if (data.code !== 0) {
      throw new Error(`Clore API error ${data.code}: ${data.error || 'unknown'}`);
    }
    return data;
  }

  async post(endpoint, body) {
    const res = await fetch(`${BASE_URL}/${endpoint}`, {
      method: 'POST',
      headers: {
        ...this.defaultHeaders,
        'Content-type': 'application/json',
      },
      body: JSON.stringify(body),
    });
    
    const data = await res.json();
    if (data.code !== 0) {
      throw new Error(`Clore API error ${data.code}: ${data.error || 'unknown'}`);
    }
    return data;
  }

  async listServers() {
    const data = await this.get('marketplace');
    return data.servers;
  }

  async getOrders(includeCompleted = false) {
    const params = includeCompleted ? { return_completed: 'true' } : {};
    const data = await this.get('my_orders', params);
    return data.orders;
  }

  async createOrder({ serverId, image, type = 'on-demand', currency = 'bitcoin', spotprice, ports, sshPassword, jupyterToken, env, command }) {
    const body = {
      currency,
      image,
      renting_server: serverId,
      type,
      ...(spotprice && { spotprice }),
      ...(ports && { ports }),
      ...(sshPassword && { ssh_password: sshPassword }),
      ...(jupyterToken && { jupyter_token: jupyterToken }),
      ...(env && { env }),
      ...(command && { command }),
    };
    return this.post('create_order', body);
  }

  async cancelOrder(orderId, issue = null) {
    const body = { id: orderId };
    if (issue) body.issue = issue;
    return this.post('cancel_order', body);
  }
}

// 用法示例
const client = new CloreClient(process.env.CLORE_API_KEY);

async function main() {
  // 列出市场
  const servers = await client.listServers();
  console.log(`市场上有 ${servers.length} 台服务器`);

  // 查找可用的 RTX 4090 服务器
  const available = servers.filter(
    s => s.specs.gpu.includes('4090') && !s.rented
  );
  console.log(`可用的 RTX 4090 服务器：${available.length}`);

  // 获取当前订单
  const orders = await client.getOrders();
  const activeOrders = orders.filter(o => !o.expired);
  console.log(`活动订单：${activeOrders.length}`);

  for (const order of activeOrders) {
    const host = order.pub_cluster?.[0];
    const portMapping = order.tcp_ports?.[0];
    if (host && portMapping) {
      const sshPort = portMapping.split(':')[1];
      console.log(`订单 ${order.id} → ssh root@${host} -p ${sshPort}`);
    }
  }
}

main().catch(console.error);
```

***

## 常见工作流

### 查找最便宜的 RTX 4090 并租用它

```python
from clore_client import CloreClient

client = CloreClient(api_key="YOUR_API_KEY")

def rent_cheapest_rtx4090(image="cloreai/ubuntu20.04-jupyter", ssh_password="SecurePass123"):
    servers = client.list_servers()
    
    # 过滤：可用的 RTX 4090
    candidates = [
        s for s in servers
        if "4090" in s["specs"].get("gpu", "")
        and not s["rented"]
    ]
    
    if not candidates:
        raise RuntimeError("未找到可用的 RTX 4090 服务器")
    
    # 按按需 BTC 价格排序
    candidates.sort(key=lambda s: s["price"]["on_demand"]["bitcoin"])
    best = candidates[0]
    
    price_btc = best["price"]["on_demand"]["bitcoin"]
    print(f"租用服务器 {best['id']}: {best['specs']['gpu']} @ {price_btc:.8f} BTC/天")
    
    client.create_order(
        server_id=best["id"],
        image=image,
        order_type="on-demand",
        currency="bitcoin",
        ports={"22": "tcp"},
        ssh_password=ssh_password,
    )
    
    print("完成！请检查您的订单以获取 SSH 连接详情。")
    return best["id"]

rent_cheapest_rtx4090()
```

***

### 监控我的订单

```python
import time
from clore_client import CloreClient

client = CloreClient(api_key="YOUR_API_KEY")

def monitor_orders(poll_interval_seconds=60):
    """轮询订单并打印状态更新。"""
    print(f"监控订单（每 {poll_interval_seconds}s 轮询）。按 Ctrl+C 停止。\n")
    
    while True:
        orders = client.get_orders(include_completed=False)
        active = [o for o in orders if not o.get("expired")]
        
        print(f"--- {len(active)} 个活动订单 ---")
        for order in active:
            cluster = order.get("pub_cluster", [])
            tcp = order.get("tcp_ports", [])
            spend = order.get("spend", 0)
            
            ssh_info = ""
            if cluster and tcp:
                port = tcp[0].split(":")[1]
                ssh_info = f" | SSH: {cluster[0]}:{port}"
            
            print(f"  订单 {order['id']}: 服务器 {order['si']}"
                  f" | 已花费 {spend:.8f} BTC{ssh_info}")
        
        if not active:
            print("  没有活动订单。")
        
        print()
        time.sleep(poll_interval_seconds)

monitor_orders()
```

***

### 当价格低于 X 时自动租用

```python
import time
from clore_client import CloreClient

client = CloreClient(api_key="YOUR_API_KEY")

def auto_rent_on_price_drop(
    gpu_model: str = "RTX 4090",
    max_price_btc: float = 0.00015,
    image: str = "cloreai/ubuntu20.04-jupyter",
    ssh_password: str = "SecurePass123",
    check_interval_seconds: int = 120,
):
    """
    监视市场并在价格低于阈值时自动租用 GPU。
    
    参数：
        gpu_model：要搜索的 GPU 名称（不区分大小写）
        max_price_btc：每天可接受的最高价格（以 BTC 为单位）
        image：要部署的 Docker 镜像
        ssh_password：容器的 SSH 密码
        check_interval_seconds：检查频率（请尊重速率限制！）
    """
    print(f"正在监视 {gpu_model}，当价格 ≤ {max_price_btc:.8f} BTC/天 时...")
    
    while True:
        servers = client.list_servers()
        
        for server in servers:
            gpu = server["specs"].get("gpu", "")
            if gpu_model.lower() not in gpu.lower():
                continue
            if server["rented"]:
                continue
            
            price = server["price"]["on_demand"]["bitcoin"]
            if price <= max_price_btc:
                print(f"🎯 找到匹配！服务器 {server['id']}: {gpu} @ {price:.8f} BTC/天")
                
                try:
                    client.create_order(
                        server_id=server["id"],
                        image=image,
                        order_type="on-demand",
                        currency="bitcoin",
                        ports={"22": "tcp"},
                        ssh_password=ssh_password,
                        required_price=price,  # 锁定此价格
                    )
                    print(f"✅ 已为服务器 {server['id']} 创建订单！")
                    return server["id"]
                except Exception as e:
                    print(f"Failed to create order: {e}. Will retry...")
        
        print(f"No match yet. Checking again in {check_interval_seconds}s...")
        time.sleep(check_interval_seconds)

auto_rent_on_price_drop(gpu_model="4090", max_price_btc=0.00012)
```

***

## WebSocket

Clore.ai 的 REST API 目前不公开 WebSocket 端点。要进行实时监控，请使用带有合理间隔的轮询（见下方速率限制）。

***

## 速率限制

| 端点                    | 限制            |
| --------------------- | ------------- |
| 大多数端点                 | **1 次请求/秒**   |
| `create_order`        | **1 次请求/5 秒** |
| `set_spot_price` （降价） | 每次 **600 秒**  |

**速率限制响应（代码 5）：**

```json
{ "code": 5 }
```

**最佳实践：**

* 添加 `time.sleep(1)` 在连续的 API 调用之间
* 对于 `create_order`，请求之间至少等待 5 秒
* 对监控循环使用 60 秒以上的轮询间隔
* 如果需要频繁查询市场数据，请在本地缓存

**Python 辅助函数：**

```python
import time

def safe_api_call(fn, *args, delay=1.1, **kwargs):
    """用速率限制保护包装 API 调用。"""
    result = fn(*args, **kwargs)
    time.sleep(delay)
    return result
```

***

## 错误处理

每个 API 响应都包含一个 `code` 字段。值为 `0` 表示成功。

### 错误代码

| 代码  | 含义                 | 操作                   |
| --- | ------------------ | -------------------- |
| `0` | 成功                 | —                    |
| `1` | 数据库错误              | 延迟后重试                |
| `2` | 输入数据无效             | 检查你的请求主体/参数          |
| `3` | API 令牌无效           | 在仪表板中验证你的 API 密钥     |
| `4` | 端点无效               | 检查端点 URL             |
| `5` | 超过速率限制（1 请求/秒）     | 在请求之间增加延迟            |
| `6` | 应用错误（见 `error` 字段） | 阅读 `error` 字段以获取详细信息 |

### 代码 6 子错误

| `error` 值                     | 含义                                    |
| ----------------------------- | ------------------------------------- |
| `exceeded_max_step`           | 现货价格下调过大；检查 `max_step` 字段             |
| `can_lower_every_600_seconds` | 必须在再次降低现货价格之前等待；检查 `time_to_lowering` |

### Python 错误处理示例

```python
from clore_client import CloreClient, CloreAPIError
import time

client = CloreClient(api_key="YOUR_API_KEY")

def create_order_with_retry(server_id, image, max_retries=3):
    for attempt in range(max_retries):
        try:
            return client.create_order(
                server_id=server_id,
                image=image,
                order_type="on-demand",
                currency="bitcoin",
                ports={"22": "tcp"},
                ssh_password="SecurePass123",
            )
        except CloreAPIError as e:
            if e.code == 5:  # Rate limit
                print(f"Rate limited. Waiting 5s... (attempt {attempt+1}/{max_retries})")
                time.sleep(5)
            elif e.code == 3:  # Bad API key
                print("Invalid API key! Check your CLORE_API_KEY.")
                raise
            elif e.code == 2:  # Bad input
                print(f"Invalid request: {e}")
                raise
            else:
                print(f"API error: {e}. Retrying in 3s...")
                time.sleep(3)
    
    raise RuntimeError(f"Failed after {max_retries} attempts")
```

### JavaScript 错误处理示例

```javascript
async function createOrderWithRetry(client, serverConfig, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await client.createOrder(serverConfig);
    } catch (err) {
      const code = parseInt(err.message.match(/error (\d+)/)?.[1]);
      
      if (code === 5) {
        console.log(`Rate limited. Waiting 5s... (attempt ${attempt + 1}/${maxRetries})`);
        await new Promise(r => setTimeout(r, 5000));
      } else if (code === 3) {
        throw new Error('Invalid API key');
      } else {
        console.log(`API error: ${err.message}. Retrying in 3s...`);
        await new Promise(r => setTimeout(r, 3000));
      }
    }
  }
  throw new Error(`Failed after ${maxRetries} attempts`);
}
```

***

## 可用的 Docker 镜像

Clore.ai 提供针对 GPU 工作负载优化的预构建镜像：

| 镜像                            | 描述                        |
| ----------------------------- | ------------------------- |
| `cloreai/ubuntu20.04-jupyter` | Ubuntu 20.04 + JupyterLab |
| `cloreai/ubuntu22.04-jupyter` | Ubuntu 22.04 + JupyterLab |

你也可以使用任何公共 Docker Hub 镜像。要使用 GPU，请使用支持 CUDA 的镜像，例如：

```
nvidia/cuda:12.1.0-devel-ubuntu22.04
```

***

## 端口转发

创建订单时，指定要暴露的端口：

```json
{
  "ports": {
    "22": "tcp",
    "8888": "http",
    "6006": "http"
  }
}
```

* **`"tcp"`** — 直接 TCP 端口转发（用于 SSH、自定义服务器）
* **`"http"`** — HTTPS 代理（用于 Jupyter、网页 UI）。每个 `http_port` 字段仅允许一个 HTTP 端口。

订单创建后，连接详情会出现在 `my_orders`:

```json
{
  "pub_cluster": ["n1.c1.clorecloud.net", "n2.c1.clorecloud.net"],
  "tcp_ports": ["22:10000"],
  "http_port": "8888"
}
```

通过 SSH 连接：

```bash
ssh root@n1.c1.clorecloud.net -p 10000
```

通过浏览器访问 Jupyter： `https://n1.c1.clorecloud.net` （使用该 `http_port`)
