openapi: 3.1.0
info:
  title: ExportDou 评论导出 API
  version: 1.0.0
  description: |
    通过持久化异步任务导出公开抖音视频下的一级评论与回复。

    所有请求都需要 Bearer API Key。创建导出任务后请保存任务 ID，按照
    `retryAfterSeconds` 查询进度，完成后读取 JSON 预览或下载完整 CSV / Excel 文件。
  license:
    name: Proprietary API; CLI is MIT licensed
    url: https://exportdou.cn/terms
  contact:
    url: https://exportdou.cn/developers/reference
externalDocs:
  description: 开发者快速开始
  url: https://exportdou.cn/developers
servers:
  - url: https://exportdou.cn/api/v1
security:
  - apiKey: []
tags:
  - name: Account
    x-displayName: 账户
    description: 验证凭证并读取账户与积分余额。
  - name: Videos
    x-displayName: 视频解析
    description: 在正式创建导出任务前解析抖音链接或分享文案。
  - name: Exports
    x-displayName: 导出任务
    description: 创建、查询、预览、下载、取消和删除持久化导出任务。
paths:
  /account:
    get:
      operationId: getAccount
      tags: [Account]
      summary: 获取当前账户
      responses:
        '200':
          description: 当前 API Key 所属账户
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Account'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /credits:
    get:
      operationId: getCredits
      tags: [Account]
      summary: 获取积分余额
      responses:
        '200':
          description: 可用、预留与历史积分统计
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Credits'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /videos/preview:
    post:
      operationId: previewDouyinVideo
      tags: [Videos]
      summary: 解析抖音视频
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [input]
              properties:
                input:
                  type: string
                  minLength: 1
                  maxLength: 8000
                  description: 标准抖音视频链接，或包含链接的完整分享文案。
                  examples:
                    - https://www.douyin.com/video/7655691431947930907
      responses:
        '200':
          description: 视频元数据与少量公开评论样本
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VideoPreview'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
  /exports:
    get:
      operationId: listExports
      tags: [Exports]
      summary: 获取导出任务列表
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: 按创建时间倒序返回的任务分页
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExportList'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      operationId: createExport
      tags: [Exports]
      summary: 创建导出任务
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
            minLength: 1
            maxLength: 200
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateExport'
      responses:
        '201':
          description: 任务已接受并完成积分预留
          content:
            application/json:
              schema:
                type: object
                required: [id, status, reservedCredits, dispatchDelayed]
                properties:
                  id:
                    type: string
                    format: uuid
                  status:
                    $ref: '#/components/schemas/ExportStatus'
                  reservedCredits:
                    type: integer
                  dispatchDelayed:
                    type: boolean
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: 可用积分不足
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: 幂等键冲突或账户并发冲突
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
  /export-requests/{idempotencyKey}:
    get:
      operationId: recoverExportByIdempotencyKey
      tags: [Exports]
      summary: 通过幂等键恢复创建结果
      parameters:
        - name: idempotencyKey
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: 与幂等键对应的导出任务
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExportDetail'
        '404':
          $ref: '#/components/responses/NotFound'
  /exports/{exportId}:
    get:
      operationId: getExport
      tags: [Exports]
      summary: 获取导出任务详情
      parameters:
        - $ref: '#/components/parameters/ExportId'
      responses:
        '200':
          description: 任务进度、计费与结果元数据
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExportDetail'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      operationId: deleteExport
      tags: [Exports]
      summary: 删除已结束的导出任务
      parameters:
        - $ref: '#/components/parameters/ExportId'
      responses:
        '204':
          description: 任务记录和私有结果文件已删除
        '409':
          description: 任务仍在运行或计费尚未结算
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /exports/{exportId}/cancel:
    post:
      operationId: cancelExport
      tags: [Exports]
      summary: 取消正在运行的任务
      parameters:
        - $ref: '#/components/parameters/ExportId'
      responses:
        '200':
          description: 取消请求后的任务状态
          content:
            application/json:
              schema:
                type: object
                required: [id, status, cancelRequested]
                properties:
                  id:
                    type: string
                    format: uuid
                  status:
                    $ref: '#/components/schemas/ExportStatus'
                  cancelRequested:
                    type: boolean
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /exports/{exportId}/preview:
    get:
      operationId: previewExportResult
      tags: [Exports]
      summary: 预览导出结果
      parameters:
        - $ref: '#/components/parameters/ExportId'
      responses:
        '200':
          description: 最终结果中最多 50 条标准化评论
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResultPreview'
        '404':
          $ref: '#/components/responses/NotFound'
  /exports/{exportId}/download:
    get:
      operationId: downloadExport
      tags: [Exports]
      summary: 下载完整结果文件
      parameters:
        - $ref: '#/components/parameters/ExportId'
      responses:
        '200':
          description: 跟随重定向后返回完整私有 CSV 或 Excel 文件
          headers:
            X-ExportDou-Row-Count:
              schema:
                type: integer
            X-ExportDou-SHA256:
              schema:
                type: string
          content:
            text/csv:
              schema:
                type: string
                format: binary
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema:
                type: string
                format: binary
        '302':
          description: 跳转到短时有效的签名下载地址
        '404':
          $ref: '#/components/responses/NotFound'
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: ed_live_...
      description: 在 https://exportdou.cn/settings/api-keys 创建，或运行 npx exportdou login 完成授权。
  parameters:
    ExportId:
      name: exportId
      in: path
      required: true
      schema:
        type: string
        format: uuid
  responses:
    BadRequest:
      description: 请求参数不合法
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: API Key 缺失、无效或已撤销
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: 资源不存在，或不属于当前凭证
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: 请求频率超过限制
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      description: 标准错误响应。
      required: [error, message]
      properties:
        error:
          type: string
          description: 稳定的机器可读错误码。
        message:
          type: string
          description: 面向开发者的错误说明。
        retryAfterSeconds:
          type: integer
          minimum: 1
          description: 建议等待多少秒后重试。
    Account:
      type: object
      description: 当前 API Key 所属的 ExportDou 账户。
      required: [id, email, displayName, accountStatus, createdAt]
      properties:
        id:
          type: string
          format: uuid
        email:
          type: [string, 'null']
        displayName:
          type: [string, 'null']
        accountStatus:
          type: string
          enum: [active, suspended, deleted]
        createdAt:
          type: string
          format: date-time
    Credits:
      type: object
      description: 账户积分余额及累计使用情况。
      required: [availableBalance, reservedBalance, lifetimeGranted, lifetimeSpent, lifetimeExpired, updatedAt]
      properties:
        availableBalance:
          type: integer
        reservedBalance:
          type: integer
        lifetimeGranted:
          type: integer
        lifetimeSpent:
          type: integer
        lifetimeExpired:
          type: integer
        updatedAt:
          type: string
          format: date-time
    VideoPreview:
      type: object
      description: 解析得到的视频基础信息、播放地址与少量评论样本。
      required: [canonicalUrl, cached, comments, playUrls]
      properties:
        canonicalUrl:
          type: string
          format: uri
        title:
          type: [string, 'null']
        authorName:
          type: [string, 'null']
        coverUrl:
          type: [string, 'null']
        reportedCommentCount:
          type: [integer, 'null']
        playUrls:
          type: array
          items:
            type: string
            format: uri
        comments:
          type: array
          items:
            $ref: '#/components/schemas/VideoPreviewComment'
        cached:
          type: boolean
    VideoPreviewComment:
      type: object
      description: 解析视频时返回的少量一级评论样本。
      required: [authorName, avatarUrl, createdAt, isAuthor, likeCount, replyCount, text]
      properties:
        authorName:
          type: [string, 'null']
        avatarUrl:
          type: [string, 'null']
        createdAt:
          type: [string, 'null']
          format: date-time
        isAuthor:
          type: boolean
        likeCount:
          type: integer
        replyCount:
          type: integer
        text:
          type: string
    CreateExport:
      type: object
      description: 创建抖音评论导出任务所需的配置。
      required: [input, resultLimit, includeReplies, format]
      properties:
        input:
          type: string
          minLength: 1
          maxLength: 8000
          description: 抖音视频链接，或包含链接的完整分享文案。
          example: https://www.douyin.com/video/7655691431947930907
        resultLimit:
          type: integer
          minimum: 1
          maximum: 200000
          description: 希望导出的总行数。开启回复后，一级评论与回复共同计数。
          example: 1000
        includeReplies:
          type: boolean
          description: 是否继续抓取一级评论下的回复。
          example: true
        format:
          type: string
          enum: [csv, xlsx]
          description: 最终结果文件格式。
          example: csv
    ExportStatus:
      type: string
      description: 导出任务当前状态。
      enum: [queued, processing, rendering, completed, partial, failed, cancelled, expired, deleting]
    ExportSummary:
      type: object
      description: 任务列表使用的精简导出信息。
      required: [id, status, resultLimit, includeReplies, format, deliveredRows, billedCredits, cancelRequested, title, createdAt, updatedAt, completedAt]
      properties:
        id:
          type: string
          format: uuid
        status:
          $ref: '#/components/schemas/ExportStatus'
        resultLimit:
          type: integer
        includeReplies:
          type: boolean
        format:
          type: string
          enum: [csv, xlsx]
        deliveredRows:
          type: integer
        billedCredits:
          type: integer
        cancelRequested:
          type: boolean
        title:
          type: [string, 'null']
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        completedAt:
          type: [string, 'null']
          format: date-time
    ExportDetail:
      description: 完整任务状态，包括进度、计费、错误和结果文件信息。
      allOf:
        - $ref: '#/components/schemas/ExportSummary'
        - type: object
          required: [inputUrl, canonicalUrl, progress, result, error, reservedCredits, releasedCredits, replyCoverage, retryAfterSeconds, startedAt]
          properties:
            inputUrl:
              type: string
            canonicalUrl:
              type: [string, 'null']
            progress:
              type: object
              description: 当前收集与文件生成进度。
              required: [stage, targetRows, deliveredRows, rootRows, replyRows, percentage]
              properties:
                stage:
                  type: string
                  enum: [queued, collecting_roots, collecting_replies, rendering, done]
                targetRows:
                  type: integer
                deliveredRows:
                  type: integer
                rootRows:
                  type: integer
                replyRows:
                  type: integer
                percentage:
                  type: integer
                  minimum: 0
                  maximum: 100
            result:
              description: 任务完成后的结果文件信息；运行中为 null。
              oneOf:
                - type: 'null'
                - type: object
                  required: [ready, format, rowCount, sizeBytes, sha256, expiresAt, previewPath, downloadPath]
                  properties:
                    ready:
                      const: true
                    format:
                      type: string
                      enum: [csv, xlsx]
                    rowCount:
                      type: integer
                    sizeBytes:
                      type: integer
                    sha256:
                      type: string
                    expiresAt:
                      type: string
                      format: date-time
                    previewPath:
                      type: string
                    downloadPath:
                      type: string
            error:
              type: [object, 'null']
            reservedCredits:
              type: integer
            releasedCredits:
              type: integer
            replyCoverage:
              type: string
              description: 回复抓取覆盖情况。
              enum: [not_requested, complete, partial, unknown]
            retryAfterSeconds:
              type: [integer, 'null']
              description: 任务仍在运行时，建议等待多少秒后再次查询。
            startedAt:
              type: [string, 'null']
              format: date-time
    ExportList:
      type: object
      description: 按时间倒序排列的任务分页。
      required: [exports, nextCursor]
      properties:
        exports:
          type: array
          items:
            $ref: '#/components/schemas/ExportSummary'
        nextCursor:
          type: [string, 'null']
    ResultPreview:
      type: object
      description: 最终导出结果中最多 50 条标准化评论，用于程序预览与轻量分析。
      required: [jobId, comments, previewCount, totalRows, rootRows, replyRows]
      properties:
        jobId:
          type: string
          format: uuid
        comments:
          type: array
          maxItems: 50
          items:
            $ref: '#/components/schemas/ResultPreviewComment'
        previewCount:
          type: integer
        totalRows:
          type: integer
        rootRows:
          type: integer
        replyRows:
          type: integer
    ResultPreviewComment:
      type: object
      description: 最终结果中的标准化一级评论或二级回复。
      required: [authorAvatarUrl, authorName, createdAt, id, isAuthor, level, likeCount, parentId, replyCount, replyToUserName, rootId, text]
      properties:
        id:
          type: string
        rootId:
          type: string
        parentId:
          type: [string, 'null']
        level:
          type: integer
          enum: [1, 2]
        authorName:
          type: [string, 'null']
        authorAvatarUrl:
          type: [string, 'null']
        text:
          type: string
        createdAt:
          type: [string, 'null']
          format: date-time
        likeCount:
          type: integer
        replyCount:
          type: integer
        isAuthor:
          type: boolean
        replyToUserName:
          type: [string, 'null']
