# 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`)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.clore.ai/clore.ai/clore.ai-eng-zh/kai-fa-zhe/cli-sdk.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
