openapi: 3.1.0
info:
  title: Jiufeng Open API
  version: "1.0.0"
  description: |
    九凤开放 API（生图）。OpenAI 兼容风格，**异步**生成：**提交 → 拿 task_id → 轮询 / Webhook**。

    本 spec 以网关 `api.jiufeng.ai/v1/*` 的**真实契约**为准（2026-08-29 校准，源快照
    `jf-ai-gateway@31a12e91`）。提交用 `{code, data:[...]}` 信封，查询用 `{code, data:{...}}` 信封，
    任务对外状态枚举为 `pending / processing / completed / failed`。
servers:
  - url: https://api.jiufeng.ai
    description: 生产
security:
  - BearerAuth: []
tags:
  - name: Images
    description: 生图提交（模型相关 API）
  - name: Tasks
    description: 异步任务查询（平台公共 API）
paths:
  /v1/images/generations:
    post:
      tags: [Images]
      summary: 提交生图任务
      description: |
        提交一个生图任务，**立即返回 task_id**（异步）。随后用 `GET /v1/tasks/{id}` 轮询，
        或提交时带 `webhook` 由平台在终态回调。图生图 / 参考图也走此端点（用 `image_urls` 传源图）。
      operationId: createImageGeneration
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ImageGenerationRequest"
            examples:
              basic:
                summary: 纯文本生图
                value:
                  model: gpt-image-2
                  prompt: 一只在霓虹城市奔跑的柴犬，赛博朋克风
                  n: 1
                  size: "16:9"
                  resolution: "1K"
              withReference:
                summary: 带参考图
                value:
                  model: gpt-image-2
                  prompt: 参考这张图的构图，换成雪山背景
                  size: "1:1"
                  resolution: "2K"
                  image_urls:
                    - https://your-cdn.example.com/ref1.png
              withWebhook:
                summary: 带 Webhook 回调
                value:
                  model: gpt-image-2
                  prompt: 一只在霓虹城市奔跑的柴犬，赛博朋克风
                  size: "16:9"
                  resolution: "1K"
                  webhook: https://your-app.example.com/hooks/jiufeng
      responses:
        "200":
          description: 已受理，返回 task_id（异步任务）
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ImageSubmitResponse"
              example:
                code: 200
                data:
                  - status: submitted
                    task_id: task_01M0CGM9MRSMD6R6PPNQWXQZ77
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientQuota" }
        "429": { $ref: "#/components/responses/RateLimited" }
      callbacks:
        webhook:
          "{$request.body#/webhook}/callback":
            post:
              summary: 终态回调（仅当请求带 webhook）
              description: |
                任务进入终态（`completed` / `failed`）时，平台向 `<webhook>/callback` 各 POST 一次。
                中间态不推；超时退款任务不推。请求头 `X-Webhook-Signature` =
                `hex(HMAC-SHA256(账号级 webhook_secret, 原始 body))`（secret 为空则不带此头）。
                重试：2xx 完成；4xx(非429) 不重试；5xx/超时/429 退避 10s/30s/60s 最多 3 次；单次超时 10s。
              parameters:
                - name: X-Webhook-Signature
                  in: header
                  required: false
                  schema: { type: string }
                  description: "hex(HMAC-SHA256(webhook_secret, rawBody))；secret 为账号设置，空则不带。"
              requestBody:
                description: "查询任务的 `data` 对象本身（不带 `{code,data}` 外壳）。"
                content:
                  application/json:
                    schema:
                      $ref: "#/components/schemas/Task"
              responses:
                "200":
                  description: 接收端 ack（返回任意 2xx 即完成，不再重试）
  /v1/tasks/{id}:
    get:
      tags: [Tasks]
      summary: 查询任务状态
      description: |
        轮询任务状态。`status` 到达终态（`completed` / `failed`）即停止轮询。
        成功时结果图在 `data.result.url`（数组，平台域名 `token-img.jiufeng.ai`，完成后 24h 过期）。
      operationId: getTaskById
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          example: task_01M0CGM9MRSMD6R6PPNQWXQZ77
        - name: language
          in: query
          required: false
          schema: { type: string, enum: [zh, en, ko, ja] }
          description: 错误文案语言
      responses:
        "200":
          description: 任务当前状态（平台信封 `{code,data}`）
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskEnvelope"
              examples:
                processing:
                  value:
                    code: 200
                    data:
                      id: task_01M0CGM9MRSMD6R6PPNQWXQZ77
                      status: processing
                      progress: "50%"
                      created: 1785076811
                completed:
                  value:
                    code: 200
                    data:
                      id: task_01M0CGM9MRSMD6R6PPNQWXQZ77
                      status: completed
                      progress: "100%"
                      created: 1785076811
                      completed: 1785076816
                      actual_time: 5
                      credits_cost: 12
                      result:
                        url:
                          - https://token-img.jiufeng.ai/f/image/xxx_0.png
                        expires_at: 1785163216
                        image_ids:
                          - img_01M0...
                failed:
                  value:
                    code: 200
                    data:
                      id: task_01M0CGM9MRSMD6R6PPNQWXQZ77
                      status: failed
                      error:
                        message: content policy violation
                        type: invalid_request_error
                        param: prompt
                        code: content_policy
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/tasks/batch:
    post:
      tags: [Tasks]
      summary: 批量查询任务（简化：仅 id + status）
      description: |
        一次查多个任务，每个任务只返回 `id` 与 `status`（不含 `result`）。`task_ids` 默认上限 500。
        除四个标准状态外，批量还可能返回 `invalid_id` / `expired` / `not_found`。
      operationId: getTasksBatch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [task_ids]
              properties:
                task_ids:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items: { type: string }
            example:
              task_ids:
                - task_01M0...
                - task_02N1...
      responses:
        "200":
          description: 各任务的 id + status
          content:
            application/json:
              schema:
                type: object
                properties:
                  code: { type: integer, example: 200 }
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        status:
                          type: string
                          enum: [pending, processing, completed, failed, invalid_id, expired, not_found]
              example:
                code: 200
                data:
                  - { id: task_01M0..., status: completed }
                  - { id: task_02N1..., status: processing }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: "你的 API 密钥：`Authorization: Bearer sk-jf-live-...`（在控制台「API 密钥」页创建）。"
  responses:
    BadRequest:
      description: 参数不合法
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Unauthorized:
      description: 未提供或无效的 API 密钥（含密钥被禁用/过期）
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Forbidden:
      description: 无权访问该资源（如查询他人任务）
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    NotFound:
      description: 任务不存在或已过期
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    InsufficientQuota:
      description: 积分不足
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    RateLimited:
      description: 触发限流，请稍后重试
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
  schemas:
    ImageGenerationRequest:
      type: object
      required: [prompt]
      properties:
        prompt:
          type: string
          description: 提示词。
        model:
          type: string
          default: gpt-image-2
          description: "当前为 gpt-image-2（更多模型即将推出）。"
        size:
          type: string
          description: "宽高比（非像素），如 16:9 / 1:1 / 9:16。"
          example: "16:9"
        resolution:
          type: string
          description: "清晰度档。"
          enum: ["1K", "2K", "4K"]
        n:
          type: integer
          default: 1
          minimum: 1
        image_urls:
          type: array
          description: "参考图 / 图生图源图 URL，最多 15 张。"
          maxItems: 15
          items: { type: string, format: uri }
        webhook:
          type: string
          format: uri
          description: "回调 base 地址；平台仅在终态 POST 到 <webhook>/callback。"
        language:
          type: string
          enum: [zh, en, ko, ja]
          description: 错误文案语言。
    ImageSubmitResponse:
      type: object
      properties:
        code: { type: integer, example: 200 }
        data:
          type: array
          items:
            type: object
            properties:
              status: { type: string, description: "初始状态，固定为 submitted。", example: submitted }
              task_id: { type: string }
    TaskEnvelope:
      type: object
      properties:
        code: { type: integer, example: 200 }
        data: { $ref: "#/components/schemas/Task" }
    Task:
      type: object
      description: "任务对象（也是 Webhook 回调的 payload 本体）。"
      properties:
        id:
          type: string
          description: "任务 id（即提交时返回的 task_id）。"
        status:
          type: string
          enum: [pending, processing, completed, failed]
          description: "对外任务状态。"
        progress: { type: string, example: "100%" }
        created: { type: integer, format: int64, description: "提交时间（Unix 秒）。" }
        estimated_time: { type: integer, description: "预估耗时（秒）。" }
        completed: { type: integer, format: int64, description: "完成时间（Unix 秒，终态才有）。" }
        actual_time: { type: integer, description: "实际耗时（秒，终态才有）。" }
        credits_cost: { type: integer, description: "消耗积分。" }
        result:
          type: object
          description: "成功时的结果。"
          properties:
            url:
              type: array
              items: { type: string, format: uri }
              description: "结果图 URL 数组（平台域名 token-img.jiufeng.ai）。"
            expires_at: { type: integer, format: int64, description: "过期时间（完成后 24h）。" }
            image_ids:
              type: array
              items: { type: string }
        error:
          type: object
          description: "失败时的错误。"
          properties:
            message: { type: string }
            type: { type: string }
            param: { type: string }
            code: { type: string }
    Error:
      type: object
      description: "OpenAI 风格错误信封（请求级失败）。"
      properties:
        error:
          type: object
          properties:
            message: { type: string }
            type: { type: string }
            code: { type: string }
