{
  "openapi": "3.1.0",
  "info": {
    "title": "博识生开放 API",
    "version": "1.0.0",
    "summary": "面向开发者的物种识别服务",
    "description": "使用客户端凭证换取短期 Token，提交生物照片并获得稳定、版本化的物种候选结果。",
    "contact": {
      "name": "博识生开放平台"
    }
  },
  "servers": [
    {
      "url": "/",
      "description": "当前服务"
    }
  ],
  "tags": [
    { "name": "认证", "description": "开发者应用凭证与访问 Token" },
    { "name": "识别", "description": "物种照片识别" },
    { "name": "应用", "description": "模型与当前应用信息" }
  ],
  "paths": {
    "/api/open/v1/token": {
      "post": {
        "tags": ["认证"],
        "summary": "换取访问 Token",
        "description": "通过 HTTP Basic 传递 Client ID 与 Client Secret。Token 默认有效 24 小时。",
        "operationId": "issueAccessToken",
        "security": [{ "clientBasic": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": ["grant_type"],
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "const": "client_credentials",
                    "description": "固定为 client_credentials"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token 已签发",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TokenResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "413": { "$ref": "#/components/responses/PayloadTooLarge" },
          "415": { "$ref": "#/components/responses/UnsupportedMediaType" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      }
    },
    "/api/open/v1/recognize": {
      "post": {
        "tags": ["识别"],
        "summary": "识别照片中的物种",
        "description": "成功识别和正常弃权都会计入每日额度与总量。上传图片不会被保存。",
        "operationId": "recognizeSpecies",
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["image"],
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary",
                    "description": "JPEG、PNG 或 WebP 图片，最大 10 MiB"
                  },
                  "model": {
                    "type": "string",
                    "enum": ["whole_knn", "legacy"],
                    "default": "whole_knn",
                    "description": "模型标识"
                  },
                  "box": {
                    "type": "string",
                    "pattern": "^[0-9]+(?:\\.[0-9]+)?,[0-9]+(?:\\.[0-9]+)?,[0-9]+(?:\\.[0-9]+)?,[0-9]+(?:\\.[0-9]+)?$",
                    "examples": ["120,80,960,720"],
                    "description": "可选原图像素坐标 x1,y1,x2,y2"
                  }
                }
              },
              "encoding": {
                "image": {
                  "contentType": "image/jpeg, image/png, image/webp"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "识别完成，也可能是正常弃权",
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" },
              "X-Quota-Daily-Limit": { "$ref": "#/components/headers/DailyLimit" },
              "X-Quota-Daily-Remaining": { "$ref": "#/components/headers/DailyRemaining" },
              "X-Quota-Total-Limit": { "$ref": "#/components/headers/TotalLimit" },
              "X-Quota-Total-Remaining": { "$ref": "#/components/headers/TotalRemaining" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RecognitionResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "413": { "$ref": "#/components/responses/PayloadTooLarge" },
          "415": { "$ref": "#/components/responses/UnsupportedMediaType" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      }
    },
    "/api/open/v1/models": {
      "get": {
        "tags": ["应用"],
        "summary": "获取可用模型",
        "description": "返回稳定模型标识和支持物种数，不公开内部训练参数。",
        "operationId": "listModels",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "可用模型列表",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": ["request_id", "models"],
                  "properties": {
                    "request_id": { "type": "string", "pattern": "^req_" },
                    "models": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Model" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      }
    },
    "/api/open/v1/me": {
      "get": {
        "tags": ["应用"],
        "summary": "获取当前应用与额度",
        "description": "返回当前 Token 对应的应用状态、三层额度和 Token 到期时间。",
        "operationId": "getCurrentApplication",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "当前应用信息",
            "headers": {
              "X-RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "X-RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" },
              "X-Quota-Daily-Limit": { "$ref": "#/components/headers/DailyLimit" },
              "X-Quota-Daily-Remaining": { "$ref": "#/components/headers/DailyRemaining" },
              "X-Quota-Total-Limit": { "$ref": "#/components/headers/TotalLimit" },
              "X-Quota-Total-Remaining": { "$ref": "#/components/headers/TotalRemaining" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApplicationInfo" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "clientBasic": {
        "type": "http",
        "scheme": "basic",
        "description": "用户名为 Client ID，密码为 Client Secret"
      },
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "opaque",
        "description": "通过 Token 接口获取的 bst_ 前缀不透明 Token"
      }
    },
    "headers": {
      "RateLimitLimit": {
        "description": "滚动 60 秒窗口上限",
        "schema": { "type": "integer", "minimum": 1 }
      },
      "RateLimitRemaining": {
        "description": "当前分钟窗口剩余次数",
        "schema": { "type": "integer", "minimum": 0 }
      },
      "RateLimitReset": {
        "description": "当前窗口重置的 Unix 时间",
        "schema": { "type": "integer", "minimum": 0 }
      },
      "DailyLimit": {
        "description": "Asia/Shanghai 自然日额度",
        "schema": { "type": "integer", "minimum": 1 }
      },
      "DailyRemaining": {
        "description": "今日剩余额度",
        "schema": { "type": "integer", "minimum": 0 }
      },
      "TotalLimit": {
        "description": "生命周期总量上限，或 unlimited",
        "schema": { "oneOf": [{ "type": "integer", "minimum": 1 }, { "type": "string", "const": "unlimited" }] }
      },
      "TotalRemaining": {
        "description": "生命周期总量剩余，或 unlimited",
        "schema": { "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "string", "const": "unlimited" }] }
      },
      "RetryAfter": {
        "description": "建议等待秒数",
        "schema": { "type": "integer", "minimum": 1 }
      }
    },
    "schemas": {
      "TokenResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["access_token", "token_type", "expires_in", "scope"],
        "properties": {
          "access_token": { "type": "string", "pattern": "^bst_", "description": "短期不透明访问 Token" },
          "token_type": { "type": "string", "const": "Bearer" },
          "expires_in": { "type": "integer", "minimum": 1, "examples": [86400] },
          "scope": { "type": "string", "const": "recognize" }
        }
      },
      "Candidate": {
        "type": "object",
        "additionalProperties": false,
        "required": ["latin", "cn", "clade", "score"],
        "properties": {
          "latin": { "type": "string", "description": "学名" },
          "cn": { "type": "string", "description": "中文名，暂无时为空字符串" },
          "clade": { "type": "string", "description": "生物类群" },
          "score": { "type": "number", "minimum": 0, "maximum": 1, "description": "候选置信度" }
        }
      },
      "Box": {
        "type": "object",
        "additionalProperties": false,
        "required": ["box", "conf"],
        "properties": {
          "box": {
            "type": "array",
            "prefixItems": [{ "type": "number" }, { "type": "number" }, { "type": "number" }, { "type": "number" }],
            "minItems": 4,
            "maxItems": 4,
            "description": "目标框像素坐标 [x1,y1,x2,y2]"
          },
          "conf": { "type": "number", "minimum": 0, "maximum": 1, "description": "目标框置信度" }
        }
      },
      "RecognitionResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["request_id", "model", "abstain", "size", "boxes", "top"],
        "properties": {
          "request_id": { "type": "string", "pattern": "^req_" },
          "model": { "type": "string", "enum": ["whole_knn", "legacy"] },
          "abstain": { "type": "boolean", "description": "true 表示无法给出可靠判断；属于正常计费结果" },
          "size": { "type": "array", "prefixItems": [{ "type": "integer" }, { "type": "integer" }], "minItems": 2, "maxItems": 2 },
          "boxes": { "type": "array", "items": { "$ref": "#/components/schemas/Box" } },
          "top": { "type": "array", "items": { "$ref": "#/components/schemas/Candidate" } }
        }
      },
      "Model": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "available", "species_count"],
        "properties": {
          "id": { "type": "string", "enum": ["whole_knn", "legacy"] },
          "species_count": { "type": "integer", "minimum": 0 },
          "available": { "type": "boolean" }
        }
      },
      "Quota": {
        "type": "object",
        "additionalProperties": false,
        "required": ["limit", "used", "remaining"],
        "properties": {
          "limit": { "oneOf": [{ "type": "integer", "minimum": 1 }, { "type": "null" }] },
          "used": { "type": "integer", "minimum": 0 },
          "remaining": { "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "null" }] },
          "reset": { "type": "integer", "minimum": 0, "description": "重置时刻的 Unix 时间；没有重置时刻时不返回" }
        }
      },
      "Application": {
        "type": "object",
        "additionalProperties": false,
        "required": ["client_id", "name", "status", "token_expires_at"],
        "properties": {
          "client_id": { "type": "string", "pattern": "^bsk_live_" },
          "name": { "type": "string" },
          "status": { "type": "string", "enum": ["active", "disabled"] },
          "token_expires_at": { "type": "integer", "minimum": 0, "description": "Token 到期的 Unix 时间" }
        }
      },
      "ApplicationInfo": {
        "type": "object",
        "additionalProperties": false,
        "required": ["request_id", "application", "quota"],
        "properties": {
          "request_id": { "type": "string", "pattern": "^req_" },
          "application": { "$ref": "#/components/schemas/Application" },
          "quota": {
            "type": "object",
            "additionalProperties": false,
            "required": ["minute", "daily", "total"],
            "properties": {
              "minute": { "$ref": "#/components/schemas/Quota" },
              "daily": { "$ref": "#/components/schemas/Quota" },
              "total": { "$ref": "#/components/schemas/Quota" }
            }
          }
        }
      },
      "ErrorDetail": {
        "type": "object",
        "additionalProperties": false,
        "required": ["code", "message", "request_id"],
        "properties": {
          "code": {
            "type": "string",
            "enum": ["invalid_request", "request_too_large", "unsupported_media_type", "unsupported_grant_type", "invalid_client", "invalid_token", "image_too_large", "invalid_image", "invalid_model", "invalid_box", "rate_limit_exceeded", "daily_quota_exceeded", "total_quota_exceeded", "service_busy", "service_unavailable", "method_not_allowed", "not_found"]
          },
          "message": { "type": "string" },
          "request_id": { "type": "string", "pattern": "^req_" }
        }
      },
      "Error": {
        "type": "object",
        "additionalProperties": false,
        "required": ["error"],
        "properties": { "error": { "$ref": "#/components/schemas/ErrorDetail" } }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "请求参数不正确",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Unauthorized": {
        "description": "凭证或 Token 无效",
        "headers": { "WWW-Authenticate": { "schema": { "type": "string" } } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "PayloadTooLarge": {
        "description": "请求体或图片超过大小限制",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "UnsupportedMediaType": {
        "description": "图片格式不支持",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "TooManyRequests": {
        "description": "达到分钟、每日或总量限制",
        "headers": {
          "Retry-After": { "$ref": "#/components/headers/RetryAfter" },
          "X-RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
          "X-RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
          "X-RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" },
          "X-Quota-Daily-Limit": { "$ref": "#/components/headers/DailyLimit" },
          "X-Quota-Daily-Remaining": { "$ref": "#/components/headers/DailyRemaining" },
          "X-Quota-Total-Limit": { "$ref": "#/components/headers/TotalLimit" },
          "X-Quota-Total-Remaining": { "$ref": "#/components/headers/TotalRemaining" }
        },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "ServiceUnavailable": {
        "description": "识别服务暂时不可用或繁忙",
        "headers": { "Retry-After": { "$ref": "#/components/headers/RetryAfter" } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    }
  }
}
