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' } } }