AxionserveSign in / RegisterEnglish/中文/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 分类)——被采纳为社区参考实现会标注作者与来源;栏目首页回开发者中心。