# 与28开放平台 / 开放接口 AI 读版

给 AI / Agent / Cursor 用：接入开放数据时优先读本文件，不要爬 HTML。
人读文档：https://yu28.top/docs
本文件 URL：https://yu28.top/developers/open-api.llms.md
Base：https://yu28.top / 更新：2026-10-08 / 时间字段：北京时间（Asia/Shanghai）

## 怎么用（给 AI）

1. 用户若用 AI 写对接代码：直接拉取并遵循本文件。
2. 人类详细说明见 /docs；本文件是精简契约，字段与路径以本文件为准。
3. 只调用下方白名单路径；不要依赖 /bff/*。

## 硬性规则

1. 仅可调用：/api/kj、/api/qh、/api/keno、/api/yk、/api/yl、/api/cl、/api/ds、/api/dx、/api/sz、/api/sha、/api/dt、/api/sf。后缀仅 .json、无后缀（JSON）或 .xml；其它后缀 404。
2. 鉴权：请求头 X-Api-Key: <key>，或 Authorization: Bearer <key>。
3. 个人 Key 禁止放进 URL ?key= 或 ?api_key=（二者同等）；仅门户 Demo Key 可用 query。误传个人 Key → 401。
4. nbr 两种含义：kj / keno / ds / dx / sz / sha 是条数（1–100，省略为 1）；qh 是期号（必填）。非法条数或期号 → 400，不改成默认值。/api/dt 的 nbr 是胜率窗口别名（同 window），不是条数。
5. /api/dt、/api/sf 的 id 是主站展示序号（如 909 或 #909），不是 UUID。
6. 错误体始终是 JSON {"error":{"code","message"}}（请求 .xml 出错也是 JSON）。XML 成功时 JSON null 写成空标签。
7. 个人 Key 数据面免费、无账号配额、不限流（读 Redis）。Demo Key：同一 IP 每分钟最多 60 次，超出 429 RATE_LIMIT。
8. 待开空值：预测接口用 JSON null；算法详情 records 待开行 time/number 用空串，hit 为 null。predict / predictNbr 尚无下期时为 null。
9. kj 与 sf.records 的 number 是三位和式（1+8+3=12）；预测接口 number 是开奖串（1+8+3），和值在 num。
10. 长龙 status 仅为中文「进行中」「已断开」，不要当枚举码。列表含两种状态。
11. 未知 category → 400。未知 sort 回落到近 20 或近 100 期胜率降序（看 window）。非法 page / pageSize 回落 1 / 20，不 400。
12. game：不传、pc28、或任何非 hx28 的值一律加拿大28（未知值不 400）。game=hx28（大小写不限）→ 400 VALIDATION「哈希28已下线」。

## 推荐连接与读取

写对接代码时按下面做，不要每秒无脑打，也不要每次新建客户端。

连接：

- GET https://yu28.top 。进程内复用一个 HTTP 客户端（Python 用 requests.Session；PHP 命令行里 curl_init 一次，循环中只改 URL，不要 curl_close）
- PHP 跟开奖必须是命令行脚本。不要放进网页请求（有执行时间上限，结束即断开）。不要用 file_get_contents（allow_url_fopen 经常关闭）。不必安装 Composer，用系统 curl 扩展。不要调用 curl_close（PHP 8.5 起弃用；8.0 起句柄离开作用域即释放）
- 请求头 X-Api-Key。Key 放环境变量 YU28_API_KEY，禁止放进 URL
- 路径用 .json。超时约 10 秒
- 400 / 401 停下来改参数或 Key。503 或网络失败隔几秒再试。不要调 /bff/*

读取（跟开奖）：

- 只打 GET /api/kj.json?nbr=1。data[0] 是最新一期。nbr 是字符串期号，与上次不同才是新开奖
- countdown 为 mm:ss 或 hh:mm:ss，表示距下一次理论开奖的剩余秒数。一期约 210 秒
- 剩余 > 15 秒：睡到大约还剩 10 秒，单次最多睡 20 秒
- 剩余 ≤ 15 秒：大约每秒读一次，直到 nbr 变化
- countdown 为 00:00 时，新期号通常还要几秒才出现。紧跟约 20 次仍没变，改为每 15 秒读一次（维护空档会停在 00:00）
- nbr 前进后再按需拉 /api/yk、/api/yl、/api/cl。已知期号用 /api/qh.json?nbr=期号
- 补历史用 nbr=条数（最多 100）。算法先 /api/dt 取 id，再 /api/sf?id=

## 鉴权

```
X-Api-Key: yu28_<16位十六进制>
```

- 形态：yu28_ + 16 hex。例：yu28_a1b2c3d4e5f60789。旧 8/32 位已作废。
- 建议环境变量：YU28_API_KEY
- 在 https://yu28.top/app?tab=api 生成（明文只显示一次；每账号最多 5 个有效 Key）

## 接口

### GET /api/kj.json?nbr={1..100}

开奖，最新在前。此处 nbr 是条数，不是期号。省略 nbr=1；非法值 400。不传 game 为加拿大28；game=hx28 → 400「哈希28已下线」；其它未知 game 仍加拿大28。

```json
{
  "countdown": "02:40",
  "data": [
    {
      "nbr": "3463701",
      "time": "2026-07-31 11:25:00",
      "number": "4+8+2=14",
      "combination": "小双"
    }
  ]
}
```

- countdown：相对最新一期的下期倒计时，可能为空字符串
- number：三位和式
- combination：大小单双组合文案

### GET /api/qh.json?nbr={期号}

按期号取单期。nbr 必填（正整数）；缺参 400；查无 404。字段与 /api/kj 单条一致；countdown 始终相对当前最新一期。

```json
{
  "countdown": "02:40",
  "data": [
    {
      "nbr": "3463701",
      "time": "2026-07-31 11:25:00",
      "number": "4+8+2=14",
      "combination": "小双"
    }
  ]
}
```

### GET /api/keno.json?nbr={1..100}

Keno 二十码源数据（仅加拿大28）。nbr 同 kj。time 为源日期与时刻拼接。

```json
{
  "data": [
    {
      "nbr": "3463701",
      "time": "2026-07-31 11:25:00",
      "nbrs": "1,4,7,9,20,23,25,26,27,31,38,39,49,52,54,56,58,62,72,74"
    }
  ]
}
```

- nbrs：20 个号码，逗号分隔、无空格

### GET /api/yk.json

已开统计（北京日历日）。不传日期参数 = 当日。`date=YYYY-MM-DD` 查单日；`from`+`to`（须同时给，含起止最多 31 个日历日）查区间。单日回 `date`，区间回 `from`/`to`，不要把区间包当单日。`data` 扁平 map。大+小约等于该范围已开期数。客户端须核身份字段。

```json
{ "date": "2026-08-30", "data": { "10": 8, "大": 102, "小单": 48 } }
```

```json
{ "from": "2026-08-28", "to": "2026-08-30", "data": { "大": 310 } }
```

### GET /api/yl.json

遗漏期数，仅 `{ "data": { "<项>": <遗漏期数> } }`，无 `date`。

### GET /api/cl.json

进行中与已断开的长龙列表。

```json
{
  "data": [
    {
      "type": "周期",
      "content": "单双",
      "status": "进行中",
      "count": 8,
      "start": "3463693",
      "current": "3463700"
    }
  ]
}
```

### GET /api/ds.json · /api/dx.json · /api/sz.json · /api/sha.json

人工智能（单双 / 大小 / 双组 / 杀组）。参数 nbr=条数（默认 1，最大 100；非法 400）。待开 number/num/result/hit 均为 null。已开 number 为开奖串（无和值），num 为和值。

```json
{
  "data": [
    {
      "nbr": "3466878",
      "predict": "双",
      "number": null,
      "num": null,
      "result": null,
      "hit": null
    }
  ]
}
```

### GET /api/dt.json

公开算法大厅列表。

参数：
- sort：rate_desc / rate_asc（配合 window）或 rate20_desc / rate20_asc / rate100_desc / rate100_asc / streak_desc / miss_desc / created_desc / favorites_desc / max_streak_desc / max_miss_desc。未知回落近窗胜率降序。主序相同走大厅复合次序（连挂再按近20胜率升序，胜率再按近100/连中/收藏/#no）
- window / nbr：胜率窗口只认 20 或 100（默认 20）。此处 nbr 不是条数
- page：≥1，默认 1；非法回落 1
- pageSize：默认 20，上限 50；非法回落 20
- category：可选 big_small | odd_even | combo_predict | kill_group | double_group；未知 400
- q：可选关键词
- game：hx28 → 400「哈希28已下线」；其它值加拿大28

```json
{
  "items": [
    {
      "id": 909,
      "name": "组合440",
      "category": "combo_predict",
      "author": "昵称",
      "hit20": "20/20",
      "hit100": "86/100",
      "streak": 24,
      "miss": 0,
      "maxStreak": 24,
      "maxMiss": 2
    }
  ],
  "page": 1,
  "pageSize": 5,
  "total": 8222,
  "sort": "rate20_desc"
}
```

- hit20 / hit100：命中比字符串；命中率 = 分子÷分母
- author：展示昵称，可能为 null
- 分类：big_small=大小，odd_even=单双，combo_predict=组合（大+单），kill_group=杀组，double_group=双组（小单+大双）
- 响应 sort 为服务端实际键；category 未筛选时为 null

### GET /api/sf.json?id={展示序号}

id 必填（展示序号，整数或 #6477）。缺参 400；不存在或未公开 404。game 规则同 kj。

在大厅字段上增加：

```json
{
  "predict": "杀大单",
  "predictNbr": "3463702",
  "records": [
    {
      "time": "",
      "nbr": "3463702",
      "predict": "杀大单",
      "number": "",
      "hit": null
    }
  ]
}
```

- predict / predictNbr：尚无下期时为 null
- records：最多 100 条已开 + 可选待开一行（插在最前）
- 已开 number 为三位和式（与 kj 相同）；待开行 time / number 为空串 ""
- hit：boolean 或 null（null = 尚未开奖）
- 建议：先 /api/dt 取 id，再调本接口

## 错误

HTTP 400 / 401 / 404 / 429 / 503 + JSON：

```json
{ "error": { "code": "UNAUTHORIZED", "message": "无效的 API Key" } }
```

常见 code：UNAUTHORIZED、VALIDATION、NOT_FOUND、RATE_LIMIT（仅 Demo Key，60 次/分钟/IP）、UNAVAILABLE、OPENAPI_UNAVAILABLE

## 最小客户端

```ts
async function yu28Get(path: string, key: string) {
  const url = new URL(path, "https://yu28.top");
  const res = await fetch(url, { headers: { "X-Api-Key": key } });
  const body = await res.json();
  if (!res.ok) {
    throw Object.assign(new Error(body?.error?.message ?? res.statusText), {
      body,
      status: res.status,
    });
  }
  return body;
}

// yu28Get("/api/kj.json?nbr=1", process.env.YU28_API_KEY!)
// yu28Get("/api/sf.json?id=909", process.env.YU28_API_KEY!)
```

```php
<?php
// 单次查询。跟开奖不要每次调用本函数：curl_init 一次留在循环外，只改 CURLOPT_URL。
function yu28_get($path, $key) {
    $ch = curl_init("https://yu28.top" . $path);
    curl_setopt_array($ch, [
        CURLOPT_HTTPHEADER => ["X-Api-Key: " . $key, "Accept: application/json"],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_USERAGENT => "yu28-php",
    ]);
    $raw = curl_exec($ch);
    $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
    if ($raw === false) {
        throw new RuntimeException("网络失败");
    }
    $body = json_decode($raw, true);
    if ($status !== 200 || !is_array($body)) {
        $message = (is_array($body) && isset($body["error"]["message"]))
            ? $body["error"]["message"]
            : $raw;
        throw new RuntimeException($message, $status);
    }
    return $body;
}
// yu28_get("/api/kj.json?nbr=1", getenv("YU28_API_KEY"));
// 数字键（已开/遗漏 JSON "10"）用 $row["10"] 或 $row[10]
```

## 权威来源

- 人读文档：https://yu28.top/docs，仓库 docs/open-api.md
- AI 读版（本文件）：https://yu28.top/developers/open-api.llms.md，仓库 docs/open-api.llms.md
- 共享 Zod：packages/shared/src/openapi/schemas.ts
- 路由实现：apps/api/src/modules/openapi/public.ts
