阿克森雲登入 / 註冊English/中文/cn繁體/tw

SDK 與庫:從 curl 到強類型客戶端

沒有官方 SDK 不是障礙:本站 API 是標準 REST + JSON + Bearer,任何語言 30 行就能封裝出可靠客戶端。本文給出封裝的五要素(鑑權/重試/錯誤/限速/類型)與 Python、Node 兩個最小實現骨架。

· AxionServe 編輯部 · 開發者

誠實開場:本站當前沒有官方 SDK——但這是低成本的缺席:API 是標準 REST + JSON + Bearer 鑑權 + 統一錯誤信封,任何語言三十行就能封裝出生產可用的客戶端。本篇給出自封裝的五要素與兩種語言的最小骨架,以及「什麼時候值得等官方 SDK」的判據。本文屬於開發者中心欄目;端點與契約先讀API 參考總覽。

客戶端封裝五要素示意

封裝五要素:一個可靠客戶端的清單

  • ①鑑權集中:令牌只在客戶端構造時注入一次(環境變量來源),所有請求自動帶頭——業務代碼裏不出現 Authorization 字符串;
  • ②錯誤翻譯:把 {error, code, requestId} 信封翻譯成語言原生異常(帶 code 屬性),調用方按異常類型分支而不是解析字符串;
  • ③重試有界:只對 5xx 與網絡錯誤重試(指數退避 + 抖動,上限 3 次);4xx 永不重試(參數錯重試一萬次還是錯),429 按 Retry-After 等待;
  • ④限速自覺:寫操作客戶端側節流(間隔 ≥2s 的令牌桶),別把 429 當重試信號用;
  • ⑤類型收口:響應用 dataclass/接口收口(哪怕只收常用字段),未知字段忽略——向前兼容的契約演進要靠調用方配合(忽略未知字段)。

Python 最小骨架(約 40 行,生產可用起點)

import os, time, json, urllib.request, urllib.error
class AxionError(Exception):
    def __init__(self, code, error, request_id): 
        super().__init__(f"[{code}] {error}"); self.code, self.request_id = code, request_id
class AxionClient:
    def __init__(self, base="https://axionserve-portal.aeronest-global.workers.dev", token=None):
        self.base, self.token = base, token or os.environ["AXN_TOKEN"]
    def _req(self, method, path, body=None, tries=3):
        for i in range(tries):
            req = urllib.request.Request(self.base + path, method=method,
                data=json.dumps(body).encode() if body else None,
                headers={"Authorization": "Bearer " + self.token, "Content-Type": "application/json",
                         "User-Agent": "my-app/1.0"})
            try:
                with urllib.request.urlopen(req, timeout=30) as r: return json.loads(r.read() or b"{}")
            except urllib.error.HTTPError as e:
                d = json.loads(e.read() or b"{}")
                if e.code == 429: time.sleep(5 * (i + 1)); continue      # 限速:等待重試
                if e.code >= 500 and i < tries - 1: time.sleep(2 ** i); continue  # 5xx:退避
                raise AxionError(d.get("code", e.code), d.get("error", str(e)), d.get("requestId"))
            except urllib.error.URLError:
                if i < tries - 1: time.sleep(2 ** i); continue
                raise
    def list_articles(self, **q): return self._req("GET", "/api/articles?" + urllib.parse.urlencode(q))
    def get_health(self): return self._req("GET", "/api/health")
# 用法:c = AxionClient(); print(c.get_health()["version"])

Node/fetch 最小骨架(同五要素)

class AxionError extends Error { constructor(c, m, rid){ super(`[${c}] ${m}`); this.code=c; this.requestId=rid; } }
class AxionClient {
  constructor(base = "https://axionserve-portal.aeronest-global.workers.dev", token = process.env.AXN_TOKEN) {
    this.base = base; this.token = token;
  }
  async req(method, path, body, tries = 3) {
    for (let i = 0; i < tries; i++) {
      const r = await fetch(this.base + path, { method,
        headers: { Authorization: `Bearer ${this.token}`, "Content-Type": "application/json" },
        body: body ? JSON.stringify(body) : undefined });
      if (r.status === 429) { await new Promise(s => setTimeout(s, 5000 * (i + 1))); continue; }
      if (r.status >= 500 && i < tries - 1) { await new Promise(s => setTimeout(s, 2 ** i * 1000)); continue; }
      const d = await r.json().catch(() => ({}));
      if (!r.ok) throw new AxionError(d.code ?? r.status, d.error ?? r.statusText, d.requestId);
      return d;
    }
    throw new AxionError(429, "rate limit exhausted", null);
  }
  listArticles(q = "") { return this.req("GET", "/api/articles" + q); }
}
// 用法:const c = new AxionClient(); console.log((await c.req("GET","/api/health")).version);

資源方法設計:從端點到動詞

骨架往上長就是 SDK:把端點映射成資源方法,調用方讀起來才像自然語言。映射規則三條:①集合用復數名詞(client.articles.list() / client.markets.get(symbol));②動作用動詞收口(client.articles.create(payload) / client.admin.login(password));③分頁透明化——列表方法默認返回第一頁 + next_cursor,提供 list_all() 迭代器自動翻頁(內部按 cursor 續拉,直到 next_cursor 爲 null)。本站列表接口的分頁契約(page/limit 與 cursor 雙模式、next_cursor 字段)在總覽與 /docs 調試臺可查,迭代器實現約 15 行。

客戶端的測試:三個必測面

  • 錯誤路徑:mock 一個 40xxx 響應,斷言異常類型與 code/requestId 屬性齊全——錯誤翻譯是客戶端最容易爛在角落的部分;
  • 重試路徑:mock 500→500→200 序列,斷言重試次數與退避間隔;mock 429 斷言等待而非立即重試;
  • 契約快照:對真實環境跑一輪只讀端點(health/articles/markets),把響應結構存成快照文件——契約漂移時快照測試先紅,比用戶先知道。

生成器路線:OpenAPI 描述 + codegen

規模大了(調用端點多於十個)再上生成器路線:把常用端點寫成 OpenAPI 3 描述文件(YAML,一次勞動),用 openapi-generator 類工具生成各語言客戶端——類型、序列化、文檔一次到位。判據:手寫骨架維護成本 > 描述文件維護成本時切換;在此之前,手寫 40 行的可控性勝過生成 4000 行的黑盒。

五要素之外的三個工程細節

  • User-Agent 報身份:客戶端固定攜帶「應用名/版本」形式的 UA——服務端日志裏能區分你的流量,出問題時工單溝通效率完全不同(「我的 UA 是 my-app/1.0,requestId 是 xxx」是最快的排障開場白);
  • 超時分層設置:連接超時短(5 秒級,連不上就快速失敗)、讀取超時按端點分層(列表 15 秒、寫操作 30 秒)——一刀切的 60 秒超時會讓故障時的隊列堆積放大十倍;
  • 冪等鍵自備:寫操作(創建類)在網絡超時後的重試有重復提交風險——本站創建端點以唯一性約束兜底(重復 slug 返回 409 衝突),你的客戶端要把「409 + 相同參數」識別爲「上次其實成功了」而不是失敗,這是分布式重試的標準語義。

語言選擇的務實口徑

自封裝客戶端選什麼語言,取決於你的調用環境而不是流行度:運維腳本與服務器內嵌選 Python(標準庫 urllib 即夠,骨架 40 行零依賴——依賴少一個,供應鏈審計就輕一分);Web 前端與 Node 服務選原生 fetch 骨架(無 SDK 依賴意味着構建鏈幹淨);Go/Rust 後端標準庫 net/http 與 reqwest 同樣是 50 行量級——任何有 HTTP 客戶端與 JSON 解析的語言都滿足封裝條件,這正是「REST + JSON + Bearer」技術選型給調用方的自由。反面判據:爲了「有類型提示」引入一個生成器黑盒,在你的端點數少於十個時幾乎總是虧本買賣——類型可以手寫 dataclass,黑盒的調試成本沒法攤薄。

什麼時候值得等官方 SDK

  • 不等:你只需要 3–10 個端點(自封裝五要素半天搞定,且你完全理解每一行);
  • 可等:你要深度集成幾十個端點 + 需要官方維護的類型同步——此時關注更新日志的 SDK 公告;
  • 永遠別等:「有 SDK 才敢調 API」——REST + JSON + 統一錯誤契約的 API,curl 就是第一個 SDK(/docs 調試臺裏每個端點都給你生成好的 curl 命令)。

相關文檔:API 參考總覽 · Agent 集成(SDK 的 Agent 化用法) · 開源項目。你封裝的客戶端歡迎發到創作社區(ai-code 分類)——被採納爲社區參考實現會標注作者與來源;欄目首頁回開發者中心。