← 返回接口文档

SDK 与客户端库

五种语言的客户端,封装了 REST 调用、实时订阅(换票据、断线自动重连、重连后的补齐和去重)和带类型的消息;Flutter、iOS、Android 另有动画直播的原生组件。各 SDK 的消息格式、重连和去重规则相同。

有哪些 SDK

SDK包含运行环境源码目录
JavaScript / TypeScriptREST 客户端、实时订阅(WebSocket)、MQTT 接入辅助、类型定义;Node 和浏览器通用,没有运行时依赖Node 22+(自带 fetch 和 WebSocket);浏览器sdk/js
PythonREST 客户端(同步 / asyncio)、实时订阅(asyncio / 同步回调)、MQTT 接入辅助;没有运行时依赖Python 3.9+sdk/python
DartREST 客户端、实时订阅、带类型的消息;纯 Dart,服务端、命令行、Flutter(含 Web)都能用Dart 3.5+sdk/dart/opendata
Flutter比赛动画 Widget:令牌方式或 App Key 方式,自动续期令牌、切后台断开、进球和完场回调Flutter 3.27+,iOS / Androidsdk/dart/opendata_anim
SwiftOpenData:REST 客户端和实时订阅;OpenDataAnim:动画组件(SwiftUI / UIKit),令牌方式或 App Key 方式iOS 15+ / macOS 12+;动画组件只在 iOSsdk/swift
Kotlinopendata:REST 客户端和实时订阅;opendata-anim:Android 动画组件,令牌方式或 App Key 方式JVM 11+ / Android(minSdk 24)sdk/kotlin

密钥规则(先读这一节)

  1. 数据 API Key 只存在于你的服务端。不要写进网页、小程序或 App 安装包,也不要下发给前端:前端里的任何东西都能被取出来。
  2. 你的服务端用 API Key 换两样短期凭证:一次性的实时票据(POST /api/v1/realtime/ticket,60 秒内有效、只能用一次)和组件令牌(POST /api/v1/widget/token,绑定配置和页面域名)。
  3. 浏览器和 App 里的 SDK 只拿票据或令牌:你给 SDK 一个回调(ticket provider / token provider),SDK 在每次连接、每次续期时调它,回调去问你的服务端。
  4. 唯一可以写进前端代码的凭证是公开的 App Key(pk_ 开头):它只能加载动画组件、调不了任何数据接口,靠域名白名单、用量上限和可选的签名模式保护。
运行位置用什么凭证怎么拿
你的服务端API Key请求头 X-API-Key;构造 SDK 的 REST 客户端时传入。带 Key 的客户端只在服务端、命令行和内部工具里用
浏览器 / App 里的实时订阅实时票据(60 秒内有效,只能用一次)你的服务端调 POST /api/v1/realtime/ticket,把结果交给前端;每次(重)连都要一张新的
网页 / App 里的动画直播组件令牌,或 App Key(pk_ 开头)令牌:你的服务端调 POST /api/v1/widget/token。App Key:控制台新建,只能加载动画,可以写在页面或 App 里

获取与安装

SDK 以源码压缩包提供,登录控制台后在「SDK 下载」里下载(每个 SDK 一个 zip,附 SHA-256 校验值);没有发布到 npm、PyPI、pub.dev、Maven Central 或 Swift 包索引。每个包解压出来是 sdk/<目录>/ 加 sdk/contract/。解压后,按下面的方式从本地目录加到你的项目里。 去控制台下载 →

unzip opendata-sdk-js-<version>.zip          # → sdk/js, sdk/contract

# 先把 SDK 构建一次(包里只有源码,没有 dist/)
cd sdk/js && npm install && npm run build

# 然后在你的项目里从这个目录安装
npm install /path/to/sdk/js

# 引用名(它 package.json 里的名字):
#   import { RestClient, RealtimeClient } from '@openplatform/football-sdk'

服务端:拉进行中的比赛,再订阅它们

下面两段在你的服务端运行,API Key 从环境变量读:先用 REST 取进行中的比赛,再订阅这些比赛的比分和事件。Key 需要开通实时推送。

import { RestClient, RealtimeClient } from '@openplatform/football-sdk'

const rest = new RestClient({ baseUrl: 'https://api.sports.csap.ai', apiKey: process.env.OPB_API_KEY })

const live = await rest.live({ limit: 50 }) // 进行中的比赛
console.log(live.length, 'live matches')

const rt = new RealtimeClient({
  rest, // 每次(重)连都用它换一张一次性票据
  subscriptions: [{ channels: ['match', 'incident'], matches: live.slice(0, 5).map((r) => r.id) }],
})
rt.on('match', (m, meta) => console.log(m.match_id, m.home_scores[0], m.away_scores[0], meta.snapshot ? '(snapshot)' : ''))
rt.on('incident', (e, meta) => console.log(e.match_id, e.incident_type, e.minute, meta.revised ? '(revised)' : ''))
rt.on('gap', (g) => console.log('offline', g.since, '→', g.until)) // 每次重连成功后一次
rt.on('fatal', (err) => console.error('will not reconnect:', err.message))
await rt.connect()

浏览器和 App:只拿票据,不放 Key

前端代码里没有 API Key。先在你的服务端加一个换票据的接口(第一段),前端的 SDK 通过回调去调它;票据只能用一次,所以每次(重)连回调都会被调用。JS SDK 在浏览器里构造带 API Key 的客户端会直接报错。

你的服务端:换票据的接口(Node 示例)

Node.js
// API Key 只在这里:你的服务端的环境变量里
import express from 'express'
import { RestClient } from '@openplatform/football-sdk'

const rest = new RestClient({ baseUrl: 'https://api.sports.csap.ai', apiKey: process.env.OPB_API_KEY })
const app = express()
// 用你自己的登录态保护这个接口:谁能调它,谁就能看实时数据
app.post('/opendata/ticket', async (_req, res) => res.json(await rest.realtimeTicket()))   // { ticket, expires_in, ws_url }
app.listen(3000)

前端:用回调拿票据

import { RealtimeClient } from '@openplatform/football-sdk'

// 页面里没有 API Key:回调去问你的服务端要票据
const rt = new RealtimeClient({
  baseUrl: 'https://api.sports.csap.ai',
  getTicket: () => fetch('/opendata/ticket', { method: 'POST' }).then((r) => r.json()),
  subscriptions: [{ channels: ['match'], matches: ['900001'] }],
})
rt.on('match', (m) => console.log(m.match_id, m.home_scores[0], m.away_scores[0]))
await rt.connect()

断线、重连与补齐

  • 服务端没有消息序号,也不重放断线期间的消息。SDK 断线后自动重连(1 秒起、每次翻倍、最长 30 秒,带随机抖动),每次重连换一张新票据,连上后重发全部订阅。
  • 重连后服务端按订阅再推一遍最新状态(snapshot: true):比赛(含整场事件表和统计)、动画、赔率。断线期间比分和事件的变化都在这份快照里。
  • SDK 只去掉内容完全相同的重复:比赛消息,版本更旧的丢弃,同版本只有内容相同才丢弃;事件,完全重复的丢弃,dkey 相同但内容不同的照常给你并标 revised(可能是对前一条的补全,也可能是同一分钟同一方的另一个事件,要你按内容判断);球位置只丢与上一帧完全相同的。断线期间没变化的比赛不会再回调一次。
  • 每次重连成功,SDK 给出一个 gap 通知(断线的起止时间)。断线期间的单条事件不会补发;需要逐条事件时,用快照里的整场事件表对账。
  • 不会再重连的情况(SDK 报致命错误):换票据返回 401 / 403(Key 无效或停用、没开实时推送、IP 不在白名单)、换票据时每日配额用完、WebSocket 升级被 403 拒绝(看得到状态码的 SDK:Python、Dart、Kotlin、Swift)、服务端以关闭码 1008 断开(实时权限被收回)、你主动关闭连接。其余失败都会退避后重试。
  • 心跳由 SDK 处理。自己写客户端时要注意:连接上有两条及以上订阅规则时,不能用重发 set 当心跳,它会把全部规则替换成这一条。订阅消息还有几个坑(退订的确切含义、写错的频道名、8192 字节上限),SDK 会在本地拦下明显的错误。

协议细节和自己实现时的规则,见实时推送文档 →

错误处理

HTTP情况SDK 的处理
401Key 缺失、无效或已停用抛出鉴权错误,不重试
403接口不在套餐内、请求 IP 不在 Key 的白名单里、没开实时推送抛出权限错误,不重试
404路径或数据不存在抛出错误,不重试
429超过每分钟限速(message 是 rate limit exceeded)等到下一个自然分钟开头再试(服务端按自然分钟计数,不返回 Retry-After),默认最多自动等 2 次,还是 429 就把错误交给你
429 当日配额用完(message 是 daily quota exceeded)抛出错误,不重试(到次日才恢复)
502 / 503 / 504网关或服务暂时不可用指数退避后自动重试,最多 2 次
500服务端错误抛出错误,不重试
—网络失败或超时只重试 GET 请求

动画直播的原生组件

Flutter、iOS、Android 各有一个组件,把平台托管的动画页放进 WebView,并负责令牌的获取和续期、退到后台时断开、进球和完场回调、高度自适应。

import 'package:flutter/widgets.dart';
import 'package:opendata_anim/opendata_anim.dart';

// 令牌方式:你的服务端用 API Key 换组件令牌(POST /api/v1/widget/token)
Widget tokenMode(Future<Map<String, Object?>> Function(Map<String, Object?> body) askMyServer) {
  return OpenDataAnim(
    baseUrl: Uri.parse('https://api.sports.csap.ai'),
    match: '900001',             // 比赛 id;'auto' 自动挑一场正在踢的;'replay:demo' 播放录像
    view: AnimView.flat25d,      // 或 AnimView.stadium3d
    tokenProvider: (req) async => AnimToken.fromJson(await askMyServer(req.toJson())),
    onEvent: (e) {
      if (e.type == AnimEventType.goal && !e.historical) debugPrint('goal ${e.side} ${e.score}');
      if (e.type == AnimEventType.fullTime) debugPrint('full time ${e.score}');
    },
  );
}

// App Key 方式:不需要自己的服务端;这把 Key 是公开的,只能加载动画
Widget appKeyMode() {
  return OpenDataAnim.appKey(
    baseUrl: Uri.parse('https://api.sports.csap.ai'),
    appKey: 'pk_xxxxxxxx',                              // 控制台 → 动画直播组件 → App Key
    embedOrigin: Uri.parse('https://app.example.com'),  // 它的主机名必须在这把 Key 的域名白名单里
    match: '900001',
    onError: (e) {
      if (e is AnimRefused) debugPrint('refused: ${e.reason}');   // domain / disabled / quota …
    },
  );
}
  • 令牌方式(三端都支持):组件向你提供的回调要组件令牌,你的服务端用 API Key 调 POST /api/v1/widget/token 换给它。
  • App Key 方式(三端都支持):不需要自己的服务端,组件用公开的 App Key 加载动画;被拒绝时组件报出原因(domain、disabled、quota 等)。
  • App 没有网址,所以 App Key 方式要给一个 embedOrigin,它的主机名必须在这把 App Key 的域名白名单里;协议要和平台地址一致(正式环境都是 https)。App 里的来源是 App 自己声明的,域名白名单拦不住别人把 Key 抄进他的 App,在意这一点请开签名模式或用令牌方式。
  • 画面(2.5D / 3D)、模板、布局由控制台里发布的那份配置决定。

组件的画面、布局、App Key 和被拒原因,见动画直播组件接入 →