Ctrl K
ComunidadGuías…

api/openapi.yaml

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
openapi: 3.1.0
info:
  title: ImageSilo API
  version: 0.1.0
  description: ImageSilo V1 的唯一 HTTP 契约来源。
servers:
  - url: /
paths:
  /healthz:
    get:
      operationId: getLiveness
      responses:
        '200':
          description: 进程存活
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
  /readyz:
    get:
      operationId: getReadiness
      responses:
        '200':
          description: SQLite 连接就绪
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/setup/status:
    get:
      operationId: getSetupStatus
      responses:
        '200':
          description: 是否已完成首次初始化
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SetupStatus' }
  /api/v1/setup:
    post:
      operationId: initializeImageSilo
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SetupRequest' }
      responses:
        '201':
          description: 首次初始化完成并创建管理员 Session
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AdminSession' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
  /api/v1/auth/login:
    post:
      operationId: login
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: 登录成功并设置安全 Session Cookie
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminSession'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /api/v1/auth/session:
    get:
      operationId: getCurrentSession
      security:
        - sessionCookie: []
      responses:
        '200':
          description: 当前管理员 Session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminSession'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/v1/auth/logout:
    post:
      operationId: logout
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '204':
          description: Session 已撤销
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/v1/auth/password:
    post:
      operationId: changePassword
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChangePasswordRequest'
      responses:
        '204':
          description: 密码已更新,当前 Session 保留,其他 Session 已撤销
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/v1/auth/profile:
    patch:
      operationId: updateAdministratorProfile
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateProfileRequest' }
      responses:
        '200':
          description: 管理员显示名称和邮箱已更新
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AdminSession' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
  /api/v1/migration-images:
    get:
      operationId: listMigrationImages
      security:
        - sessionCookie: []
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 24 }
        - name: cursor
          in: query
          schema: { type: string }
        - name: q
          in: query
          description: 按迁移图片文件名或完整公网路径搜索
          schema: { type: string, maxLength: 256 }
        - name: format
          in: query
          schema: { type: string, enum: [jpeg, png, webp, gif] }
        - name: minBytes
          in: query
          schema: { type: integer, format: int64, minimum: 0 }
        - name: maxBytes
          in: query
          schema: { type: integer, format: int64, minimum: 0 }
      responses:
        '200':
          description: 迁移目录中可公开交付图片的平铺分页列表
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MigrationImageList' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/v1/migration-images/batch-delete:
    post:
      operationId: batchDeleteMigrationImages
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BatchMigrationImageRequest' }
      responses:
        '200':
          description: 永久删除迁移图片并递归清理空父目录后的逐项结果
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MigrationImageBatchResult' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/v1/migration-images/refresh:
    post:
      operationId: refreshMigrationImages
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '204':
          description: 已重新扫描迁移目录并替换文件列表快照
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /api/v1/images:
    get:
      operationId: listImages
      security:
        - sessionCookie: []
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - name: cursor
          in: query
          schema: { type: string }
        - name: q
          in: query
          description: 按原始文件名、标准路径 UUID、历史路径或 SHA-256 搜索
          schema: { type: string, maxLength: 200 }
        - name: visibility
          in: query
          schema: { $ref: '#/components/schemas/Visibility' }
        - name: format
          in: query
          schema: { type: string, enum: [jpeg, png, webp, gif] }
        - name: uploadedVia
          in: query
          schema: { type: string, enum: [admin, api_token, import] }
        - name: createdFrom
          in: query
          schema: { type: string, format: date-time }
        - name: createdTo
          in: query
          schema: { type: string, format: date-time }
        - name: minBytes
          in: query
          schema: { type: integer, format: int64, minimum: 0 }
        - name: maxBytes
          in: query
          schema: { type: integer, format: int64, minimum: 0 }
        - name: minWidth
          in: query
          schema: { type: integer, minimum: 0 }
        - name: maxWidth
          in: query
          schema: { type: integer, minimum: 0 }
        - name: minHeight
          in: query
          schema: { type: integer, minimum: 0 }
        - name: maxHeight
          in: query
          schema: { type: integer, minimum: 0 }
      responses:
        '200':
          description: 使用稳定 keyset cursor 的分页图片列表
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImageList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '400':
          $ref: '#/components/responses/BadRequest'
    post:
      operationId: uploadImage
      security:
        - sessionCookie: []
        - bearerToken: ['images:upload']
      parameters:
        - $ref: '#/components/parameters/CSRFTokenForSession'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                visibility:
                  $ref: '#/components/schemas/Visibility'
      responses:
        '201':
          description: 图片上传成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Image'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/images/batch-delete:
    post:
      operationId: batchDeleteImages
      security:
        - sessionCookie: []
        - bearerToken: ['images:delete']
      parameters:
        - $ref: '#/components/parameters/CSRFTokenForSession'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BatchImageRequest' }
      responses:
        '200':
          description: 每张图片的永久删除结果;文件清理失败会明确标记
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BatchOperationResult' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/v1/images/batch-visibility:
    patch:
      operationId: batchUpdateImageVisibility
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BatchVisibilityRequest' }
      responses:
        '200':
          description: 每张图片的可见性修改结果
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BatchOperationResult' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/v1/images/{imageId}:
    get:
      operationId: getImageDetail
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/ImageId'
      responses:
        '200':
          description: 图片完整管理详情及历史别名
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ImageDetail' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      operationId: deleteImage
      security:
        - sessionCookie: []
        - bearerToken: ['images:delete']
      parameters:
        - $ref: '#/components/parameters/ImageId'
        - $ref: '#/components/parameters/CSRFTokenForSession'
      responses:
        '200':
          description: 数据库和索引已永久删除;响应说明文件清理是否待巡检
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeleteImageResult' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/v1/images/{imageId}/visibility:
    patch:
      operationId: updateImageVisibility
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/ImageId'
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateImageVisibilityRequest'
      responses:
        '204':
          description: 可见性已提交到 SQLite 并同步更新交付索引
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/v1/images/{imageId}/convert-webp:
    post:
      operationId: convertImageToWebP
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/ImageId'
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '200':
          description: 保持图片 ID 和 URL 不变,将现有 JPEG 或 PNG 原子替换为 WebP
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebPConversionResult' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/images/{imageId}/thumbnail:
    get:
      operationId: getImageThumbnail
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/ImageId'
      responses:
        '200':
          description: 可重建的后台 JPEG 预览缩略图
          content:
            image/jpeg:
              schema: { type: string, format: binary }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /image/{imageId}:
    get:
      operationId: deliverImage
      security:
        - {}
        - sessionCookie: []
        - bearerToken: ['images:read_private']
      parameters:
        - $ref: '#/components/parameters/ImageId'
      responses:
        '200':
          description: 图片字节;私密图片需要 Session 或 images:read_private Scope
          headers:
            ETag:
              schema: { type: string }
            Last-Modified:
              schema: { type: string }
            Cache-Control:
              schema: { type: string }
          content:
            image/jpeg: { schema: { type: string, format: binary } }
            image/png: { schema: { type: string, format: binary } }
            image/webp: { schema: { type: string, format: binary } }
            image/gif: { schema: { type: string, format: binary } }
        '206':
          description: Range 局部响应
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/api-tokens:
    get:
      operationId: listApiTokens
      security:
        - sessionCookie: []
      responses:
        '200':
          description: API Token 元数据列表,永不返回 Token 明文
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiTokenList'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      operationId: createApiToken
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateApiTokenRequest'
      responses:
        '201':
          description: Token 明文仅在本响应返回一次
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatedApiToken'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/v1/api-tokens/{tokenId}:
    delete:
      operationId: revokeApiToken
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/TokenId'
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '204':
          description: Token 已吊销并立即从活跃内存索引移除
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/v1/settings:
    get:
      operationId: getSettings
      security:
        - sessionCookie: []
      responses:
        '200':
          description: 当前可由阶段 2 管理的系统设置
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppSettings'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/v1/settings/default-visibility:
    patch:
      operationId: updateDefaultVisibility
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateDefaultVisibilityRequest'
      responses:
        '200':
          description: 默认可见性已更新
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppSettings'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/v1/settings/processing:
    patch:
      operationId: updateProcessingSettings
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateProcessingSettingsRequest'
      responses:
        '200':
          description: 图片压缩与 WebP 转换设置已更新
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppSettings'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/v1/system:
    get:
      operationId: getSystemInfo
      security:
        - sessionCookie: []
      responses:
        '200':
          description: 只读处理并发与上传安全边界
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SystemInfo'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/v1/overview:
    get:
      operationId: getSystemOverview
      security:
        - sessionCookie: []
      responses:
        '200':
          description: 图片、存储、运行内存、索引和最近维护结果的最小概览
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SystemOverview' }
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/v1/maintenance/rebuild:
    post:
      operationId: rebuildIndexes
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '200':
          description: 全部内存索引已在独占变更屏障中整体重建
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RebuildResult' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/v1/maintenance/inspect:
    post:
      operationId: inspectConsistency
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/CSRFToken'
      responses:
        '200':
          description: 只读一致性巡检结果;本阶段不自动删除文件
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InspectionResult' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api/v1/imports:
    post:
      operationId: importImage
      security:
        - sessionCookie: []
        - bearerToken: ['images:upload', 'aliases:write']
      parameters:
        - $ref: '#/components/parameters/CSRFTokenForSession'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file, alias]
              properties:
                file: { type: string, format: binary }
                alias: { type: string, maxLength: 2048, example: /i/2022/05/example.webp }
                visibility:
                  $ref: '#/components/schemas/Visibility'
      responses:
        '201':
          description: 原始图片与一个历史别名成功导入
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /api/v1/aliases:
    get:
      operationId: listAliases
      security:
        - sessionCookie: []
        - bearerToken: ['aliases:write']
      parameters:
        - name: limit
          in: query
          required: false
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - name: cursor
          in: query
          required: false
          schema: { type: string, maxLength: 512 }
      responses:
        '200':
          description: 历史路径映射列表
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImageAliasList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      operationId: createAlias
      security:
        - sessionCookie: []
        - bearerToken: ['aliases:write']
      parameters:
        - $ref: '#/components/parameters/CSRFTokenForSession'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAliasRequest'
      responses:
        '201':
          description: 别名创建成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImageAlias'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
  /api/v1/aliases/resolve:
    get:
      operationId: resolveAlias
      security:
        - sessionCookie: []
        - bearerToken: ['aliases:write']
      parameters:
        - name: path
          in: query
          required: true
          schema: { type: string, maxLength: 2048 }
      responses:
        '200':
          description: 已解析的历史路径映射
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImageAlias'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/v1/aliases/{aliasId}:
    delete:
      operationId: deleteAlias
      security:
        - sessionCookie: []
        - bearerToken: ['aliases:write']
      parameters:
        - $ref: '#/components/parameters/AliasId'
        - $ref: '#/components/parameters/CSRFTokenForSession'
      responses:
        '204':
          description: 历史路径映射已删除
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  securitySchemes:
    sessionCookie:
      type: apiKey
      in: cookie
      name: imagesilo_session
    bearerToken:
      type: http
      scheme: bearer
  parameters:
    ImageId:
      name: imageId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    TokenId:
      name: tokenId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    AliasId:
      name: aliasId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    CSRFToken:
      name: X-CSRF-Token
      in: header
      required: true
      description: 必须与当前 Session 绑定的 imagesilo_csrf Cookie 完全一致。
      schema:
        type: string
        pattern: '^isc_[A-Za-z0-9_-]{43}$'
    CSRFTokenForSession:
      name: X-CSRF-Token
      in: header
      required: false
      description: 使用管理员 Session 上传时必填;使用 Bearer Token 时忽略。
      schema:
        type: string
        pattern: '^isc_[A-Za-z0-9_-]{43}$'
  schemas:
    Visibility:
      type: string
      enum: [public, private]
    ApiTokenScope:
      type: string
      enum: ['images:upload', 'images:read_private', 'images:delete', 'aliases:write']
    HealthResponse:
      type: object
      required: [status]
      properties:
        status: { type: string }
    LoginRequest:
      type: object
      additionalProperties: false
      required: [email, password]
      properties:
        email: { type: string, format: email, maxLength: 254 }
        password: { type: string, minLength: 1, maxLength: 1024 }
    SetupStatus:
      type: object
      required: [initialized]
      properties:
        initialized: { type: boolean }
    SetupRequest:
      type: object
      additionalProperties: false
      required: [bootstrapToken, displayName, email, password, defaultVisibility, compressionEnabled, jpegQuality, webpQuality, pngCompressionLevel, conversionEnabled, conversionWebpQuality, conversionWebpLossless]
      properties:
        bootstrapToken: { type: string, pattern: '^isb_[A-Za-z0-9_-]{43}$' }
        displayName: { type: string, minLength: 1, maxLength: 80 }
        email: { type: string, format: email, maxLength: 254 }
        password: { type: string, minLength: 12, maxLength: 1024 }
        defaultVisibility: { $ref: '#/components/schemas/Visibility' }
        compressionEnabled: { type: boolean }
        jpegQuality: { type: integer, minimum: 1, maximum: 100 }
        webpQuality: { type: integer, minimum: 1, maximum: 100 }
        pngCompressionLevel: { type: integer, minimum: 0, maximum: 9 }
        conversionEnabled: { type: boolean }
        conversionWebpQuality: { type: integer, minimum: 1, maximum: 100 }
        conversionWebpLossless: { type: boolean }
    AdminSession:
      type: object
      required: [adminId, displayName, email, csrfToken, expiresAt]
      properties:
        adminId: { type: string, format: uuid }
        displayName: { type: string }
        email: { type: string, format: email }
        csrfToken: { type: string, pattern: '^isc_[A-Za-z0-9_-]{43}$' }
        expiresAt: { type: string, format: date-time }
    ChangePasswordRequest:
      type: object
      additionalProperties: false
      required: [currentPassword, newPassword]
      properties:
        currentPassword: { type: string, minLength: 1, maxLength: 1024 }
        newPassword: { type: string, minLength: 12, maxLength: 1024 }
    UpdateProfileRequest:
      type: object
      additionalProperties: false
      required: [displayName, email]
      properties:
        displayName: { type: string, minLength: 1, maxLength: 80 }
        email: { type: string, format: email, maxLength: 254 }
    Image:
      type: object
      required: [id, originalName, mimeType, extension, width, height, sourceSize, storedSize, sourceSha256, storedSha256, processingSummary, visibility, uploadedVia, standardUrl, thumbnailUrl, createdAt]
      properties:
        id: { type: string, format: uuid }
        originalName: { type: string }
        mimeType: { type: string, enum: [image/jpeg, image/png, image/webp, image/gif] }
        extension: { type: string, enum: [.jpg, .png, .webp, .gif] }
        width: { type: integer, minimum: 1 }
        height: { type: integer, minimum: 1 }
        sourceSize: { type: integer, format: int64, minimum: 1 }
        storedSize: { type: integer, format: int64, minimum: 1 }
        sourceSha256: { type: string, pattern: '^[0-9a-f]{64}$' }
        storedSha256: { type: string, pattern: '^[0-9a-f]{64}$' }
        processingSummary: { $ref: '#/components/schemas/ProcessingSummary' }
        visibility: { $ref: '#/components/schemas/Visibility' }
        uploadedVia: { type: string, enum: [admin, api_token, import] }
        standardUrl: { type: string }
        thumbnailUrl: { type: string }
        createdAt: { type: string, format: date-time }
    ImageList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Image' }
        nextCursor: { type: string }
    MigrationImage:
      type: object
      required: [path, originalName, mimeType, extension, storedSize, standardUrl, modifiedAt]
      properties:
        path: { type: string, maxLength: 2048, pattern: '^/' }
        originalName: { type: string }
        mimeType: { type: string, enum: [image/jpeg, image/png, image/webp, image/gif] }
        extension: { type: string, enum: [.jpg, .jpeg, .png, .webp, .gif] }
        storedSize: { type: integer, format: int64, minimum: 1 }
        standardUrl: { type: string, maxLength: 2048, pattern: '^/' }
        modifiedAt: { type: string, format: date-time }
    MigrationImageList:
      type: object
      required: [items, skippedFiles, mutationsEnabled]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/MigrationImage' }
        nextCursor: { type: string }
        skippedFiles: { type: integer, minimum: 0 }
        mutationsEnabled: { type: boolean }
    BatchMigrationImageRequest:
      type: object
      additionalProperties: false
      required: [paths]
      properties:
        paths:
          type: array
          minItems: 1
          maxItems: 100
          uniqueItems: true
          items: { type: string, maxLength: 2048, pattern: '^/' }
    MigrationImageOperationItem:
      type: object
      required: [path, status]
      properties:
        path: { type: string, maxLength: 2048, pattern: '^/' }
        status: { type: string, enum: [deleted, cleanup_pending, not_found, error] }
        removedDirectories: { type: integer, minimum: 0 }
        directoryCleanupPending: { type: boolean }
        errorCode: { type: string }
    MigrationImageBatchResult:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/MigrationImageOperationItem' }
    ImageDetail:
      allOf:
        - $ref: '#/components/schemas/Image'
        - type: object
          required: [aliases]
          properties:
            aliases:
              type: array
              items: { $ref: '#/components/schemas/ImageAlias' }
    DeleteImageResult:
      type: object
      required: [imageId, imageFileDeleted, thumbnailDeleted, cleanupPending]
      properties:
        imageId: { type: string, format: uuid }
        imageFileDeleted: { type: boolean }
        thumbnailDeleted: { type: boolean }
        cleanupPending: { type: boolean }
    WebPConversionResult:
      type: object
      required: [image, originalFileDeleted, thumbnailUpdated, cleanupPending]
      properties:
        image: { $ref: '#/components/schemas/Image' }
        originalFileDeleted: { type: boolean }
        thumbnailUpdated: { type: boolean }
        cleanupPending: { type: boolean }
    BatchImageRequest:
      type: object
      additionalProperties: false
      required: [imageIds]
      properties:
        imageIds:
          type: array
          minItems: 1
          maxItems: 100
          uniqueItems: true
          items: { type: string, format: uuid }
    BatchVisibilityRequest:
      allOf:
        - $ref: '#/components/schemas/BatchImageRequest'
        - type: object
          required: [visibility]
          properties:
            visibility: { $ref: '#/components/schemas/Visibility' }
    BatchOperationItem:
      type: object
      required: [imageId, status]
      properties:
        imageId: { type: string, format: uuid }
        status: { type: string, enum: [deleted, cleanup_pending, updated, not_found, error] }
        cleanupPending: { type: boolean }
        errorCode: { type: string }
    BatchOperationResult:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/BatchOperationItem' }
    UpdateImageVisibilityRequest:
      type: object
      additionalProperties: false
      required: [visibility]
      properties:
        visibility: { $ref: '#/components/schemas/Visibility' }
    CreateApiTokenRequest:
      type: object
      additionalProperties: false
      required: [name, scopes]
      properties:
        name: { type: string, minLength: 1, maxLength: 100 }
        scopes:
          type: array
          minItems: 1
          uniqueItems: true
          items: { $ref: '#/components/schemas/ApiTokenScope' }
        expiresAt: { type: [string, 'null'], format: date-time }
    CreatedApiToken:
      type: object
      required: [id, name, token, tokenPrefix, scopes, expiresAt, status, createdAt]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        token: { type: string, writeOnly: true, pattern: '^ist_[A-Za-z0-9_-]{43}$' }
        tokenPrefix: { type: string }
        scopes:
          type: array
          items: { $ref: '#/components/schemas/ApiTokenScope' }
        expiresAt: { type: [string, 'null'], format: date-time }
        status: { type: string, enum: [active] }
        createdAt: { type: string, format: date-time }
    ApiToken:
      type: object
      required: [id, name, tokenPrefix, scopes, expiresAt, status, createdAt]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        tokenPrefix: { type: string }
        scopes:
          type: array
          items: { $ref: '#/components/schemas/ApiTokenScope' }
        expiresAt: { type: [string, 'null'], format: date-time }
        status: { type: string, enum: [active, expired, revoked] }
        createdAt: { type: string, format: date-time }
    ApiTokenList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/ApiToken' }
    AppSettings:
      type: object
      required: [defaultVisibility, compressionEnabled, jpegQuality, webpQuality, pngCompressionLevel, conversionEnabled, conversionWebpQuality, conversionWebpLossless]
      properties:
        defaultVisibility: { $ref: '#/components/schemas/Visibility' }
        compressionEnabled: { type: boolean }
        jpegQuality: { type: integer, minimum: 1, maximum: 100 }
        webpQuality: { type: integer, minimum: 1, maximum: 100 }
        pngCompressionLevel: { type: integer, minimum: 0, maximum: 9 }
        conversionEnabled: { type: boolean }
        conversionWebpQuality: { type: integer, minimum: 1, maximum: 100 }
        conversionWebpLossless: { type: boolean }
    UpdateDefaultVisibilityRequest:
      type: object
      additionalProperties: false
      required: [defaultVisibility]
      properties:
        defaultVisibility: { $ref: '#/components/schemas/Visibility' }
    UpdateProcessingSettingsRequest:
      type: object
      additionalProperties: false
      required: [compressionEnabled, jpegQuality, webpQuality, pngCompressionLevel, conversionEnabled, conversionWebpQuality, conversionWebpLossless]
      properties:
        compressionEnabled: { type: boolean }
        jpegQuality: { type: integer, minimum: 1, maximum: 100 }
        webpQuality: { type: integer, minimum: 1, maximum: 100 }
        pngCompressionLevel: { type: integer, minimum: 0, maximum: 9 }
        conversionEnabled: { type: boolean }
        conversionWebpQuality: { type: integer, minimum: 1, maximum: 100 }
        conversionWebpLossless: { type: boolean }
    ProcessingSummary:
      type: object
      required: [action, sourceFormat, storedFormat, preserved, compressionEnabled, conversionEnabled]
      properties:
        action: { type: string, enum: [preserve, compress, convert] }
        sourceFormat: { type: string, enum: [jpeg, png, webp, gif] }
        storedFormat: { type: string, enum: [jpeg, png, webp, gif] }
        preserved: { type: boolean }
        compressionEnabled: { type: boolean }
        conversionEnabled: { type: boolean }
        compressionRejected: { type: boolean }
    SystemInfo:
      type: object
      required: [processingConcurrency, deliveryConcurrency, maxBatchCount, maxUploadBytes, maxTotalPixels, supportedFormats, vipsVersion]
      properties:
        processingConcurrency: { type: integer, minimum: 1 }
        deliveryConcurrency:
          type: integer
          minimum: 0
          description: Zero means image delivery concurrency is unlimited.
        maxBatchCount: { type: integer, minimum: 1 }
        maxUploadBytes: { type: integer, format: int64, minimum: 1 }
        maxTotalPixels: { type: integer, format: int64, minimum: 1 }
        supportedFormats:
          type: array
          items: { type: string, enum: [image/jpeg, image/png, image/webp, image/gif] }
        vipsVersion: { type: string }
    IndexStats:
      type: object
      required: [images, aliases, sessions, tokens]
      properties:
        images: { type: integer, minimum: 0 }
        aliases: { type: integer, minimum: 0 }
        sessions: { type: integer, minimum: 0 }
        tokens: { type: integer, minimum: 0 }
    InspectionResult:
      type: object
      required: [checkedAt, databaseImages, imageFiles, thumbnailFiles, temporaryFiles, missingImageCount, missingImageIds, orphanImageCount, orphanThumbnailCount]
      properties:
        checkedAt: { type: string, format: date-time }
        databaseImages: { type: integer, minimum: 0 }
        imageFiles: { type: integer, minimum: 0 }
        thumbnailFiles: { type: integer, minimum: 0 }
        temporaryFiles: { type: integer, minimum: 0 }
        missingImageCount: { type: integer, minimum: 0 }
        missingImageIds:
          type: array
          maxItems: 100
          items: { type: string, format: uuid }
        orphanImageCount: { type: integer, minimum: 0 }
        orphanThumbnailCount: { type: integer, minimum: 0 }
    RebuildResult:
      type: object
      required: [completedAt, images, aliases, sessions, tokens, missingImageCount, missingImageIds]
      properties:
        completedAt: { type: string, format: date-time }
        images: { type: integer, minimum: 0 }
        aliases: { type: integer, minimum: 0 }
        sessions: { type: integer, minimum: 0 }
        tokens: { type: integer, minimum: 0 }
        missingImageCount: { type: integer, minimum: 0 }
        missingImageIds:
          type: array
          maxItems: 100
          items: { type: string, format: uuid }
    DailyMaintenanceResult:
      type: object
      required: [completedAt, inspection, removedTemporaryFiles, removedOrphanImages, removedOrphanThumbnails, cleanupFailures, indexConsistent]
      properties:
        completedAt: { type: string, format: date-time }
        inspection: { $ref: '#/components/schemas/InspectionResult' }
        removedTemporaryFiles: { type: integer, minimum: 0 }
        removedOrphanImages: { type: integer, minimum: 0 }
        removedOrphanThumbnails: { type: integer, minimum: 0 }
        cleanupFailures: { type: integer, minimum: 0 }
        indexConsistent: { type: boolean }
    SystemOverview:
      type: object
      required: [imageCount, storedBytes, migrationStoredBytes, aliasCount, heapAllocBytes, heapSysBytes, rssBytes, goroutines, indexes, indexConsistent, missingImageCount, missingImageIds, lastInspection, lastRebuild, lastDaily]
      properties:
        imageCount: { type: integer, format: int64, minimum: 0 }
        storedBytes: { type: integer, format: int64, minimum: 0 }
        migrationStoredBytes: { type: integer, format: int64, minimum: 0 }
        aliasCount: { type: integer, format: int64, minimum: 0 }
        heapAllocBytes: { type: integer, format: int64, minimum: 0 }
        heapSysBytes: { type: integer, format: int64, minimum: 0 }
        rssBytes: { type: integer, format: int64, minimum: 0 }
        goroutines: { type: integer, minimum: 0 }
        indexes: { $ref: '#/components/schemas/IndexStats' }
        indexConsistent: { type: boolean }
        missingImageCount: { type: integer, minimum: 0 }
        missingImageIds:
          type: array
          maxItems: 100
          items: { type: string, format: uuid }
        lastInspection:
          oneOf:
            - $ref: '#/components/schemas/InspectionResult'
            - type: 'null'
        lastRebuild:
          oneOf:
            - $ref: '#/components/schemas/RebuildResult'
            - type: 'null'
        lastDaily:
          oneOf:
            - $ref: '#/components/schemas/DailyMaintenanceResult'
            - type: 'null'
    CreateAliasRequest:
      type: object
      additionalProperties: false
      required: [path, imageId, source]
      properties:
        path: { type: string }
        imageId: { type: string, format: uuid }
        source: { type: string, minLength: 1, maxLength: 100 }
    ImageAlias:
      type: object
      required: [id, path, imageId, source, createdAt]
      properties:
        id: { type: string, format: uuid }
        path: { type: string }
        imageId: { type: string, format: uuid }
        source: { type: string }
        createdAt: { type: string, format: date-time }
    ImageAliasList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/ImageAlias' }
        nextCursor: { type: string }
    ImportResult:
      type: object
      required: [imageId, standardUrl, sha256, alias]
      properties:
        imageId: { type: string, format: uuid }
        standardUrl: { type: string }
        sha256: { type: string, pattern: '^[0-9a-f]{64}$' }
        alias: { $ref: '#/components/schemas/ImageAlias' }
    Error:
      type: object
      required: [code, message, requestId]
      properties:
        code: { type: string }
        message: { type: string }
        requestId: { type: string }
        fields:
          type: object
          additionalProperties: { type: string }
  responses:
    BadRequest:
      description: 请求无效
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Unauthorized:
      description: 缺少或认证信息无效
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Forbidden:
      description: 已认证但 Scope 不足
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    NotFound:
      description: 资源不存在
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Conflict:
      description: 唯一路径冲突
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    TooManyRequests:
      description: 请求被限速
      headers:
        Retry-After: { schema: { type: integer, minimum: 1 } }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    InternalServerError:
      description: 服务器无法完成请求
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    ServiceUnavailable:
      description: 服务未就绪或图片解码安全门满载
      headers:
        Retry-After: { schema: { type: integer, minimum: 1 } }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }