feat(storage): 统一二进制对象存储与公开错误

新增 Aliyun OSS 与 S3 协议、通道内重试和按优先级跨通道切换,保留 server-main 兼容与环境 OSS 内存通道。

将请求及结果中的 Base64、Data URI、Buffer、multipart 和内联二进制统一对象化,生产路径不再写入本机静态目录,历史本地资源仅保留只读兼容。

引入 PublicErrorV1 并统一 API、异步查询、兼容协议和失败回调的安全错误输出,同时补充迁移、管理端、指标、OpenAPI 与本地模拟验收。

验证:go test ./... -count=1;go vet ./...;pnpm lint;pnpm test;pnpm build;pnpm openapi;tests/ci/migrations-test.sh。
This commit is contained in:
2026-08-04 08:14:39 +08:00
parent d129bcccbd
commit 0f0998cbcf
55 changed files with 3649 additions and 1008 deletions
+182 -16
View File
@@ -3157,7 +3157,7 @@
"BearerAuth": []
}
],
"description": "创建文件存储通道,当前主要用于配置 server-main OpenAPI 上传通道。",
"description": "创建 server-main OpenAPI、阿里云 OSS 或 S3 兼容文件存储通道。",
"consumes": [
"application/json"
],
@@ -3354,6 +3354,70 @@
}
}
},
"/api/admin/system/file-storage/channels/{channelID}/test": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "对指定 OSS 或 S3 通道执行隔离的 Put、Head、Delete 探针,不返回凭据或对象键。",
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "测试对象存储通道",
"parameters": [
{
"type": "string",
"description": "文件存储通道 ID",
"name": "channelID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/httpapi.FileStorageChannelTestResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"503": {
"description": "Service Unavailable",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
}
}
}
},
"/api/admin/system/file-storage/settings": {
"get": {
"security": [
@@ -6712,7 +6776,7 @@
"BearerAuth": []
}
],
"description": "上传文件到配置的文件存储通道;没有启用通道时回退到本地静态上传目录。单文件最大 256MiB。",
"description": "上传文件到配置的对象存储通道;所有通道失败时返回标准化存储错误,不写入本机静态目录。单文件最大 256MiB。",
"consumes": [
"multipart/form-data"
],
@@ -11207,22 +11271,52 @@
"httpapi.ErrorPayload": {
"type": "object",
"properties": {
"action": {
"type": "string",
"example": "retry_after"
},
"category": {
"type": "string",
"example": "rate_limit"
},
"code": {
"type": "string",
"example": "rate_limit"
},
"httpStatus": {
"type": "integer",
"example": 429
},
"message": {
"type": "string",
"example": "invalid json body"
},
"param": {},
"requestId": {
"type": "string"
},
"retryAfterSeconds": {
"type": "integer",
"example": 2
},
"retryable": {
"type": "boolean",
"example": true
},
"status": {
"type": "integer",
"example": 400
},
"taskId": {
"type": "string"
},
"type": {
"type": "string",
"example": "invalid_request_error"
},
"version": {
"type": "string",
"example": "v1"
}
}
},
@@ -11237,6 +11331,26 @@
}
}
},
"httpapi.FileStorageChannelTestResponse": {
"type": "object",
"properties": {
"deleteSucceeded": {
"type": "boolean"
},
"durationMs": {
"type": "integer"
},
"headSucceeded": {
"type": "boolean"
},
"provider": {
"type": "string"
},
"putSucceeded": {
"type": "boolean"
}
}
},
"httpapi.FileUploadData": {
"type": "object",
"properties": {
@@ -11443,6 +11557,9 @@
"example": 0
},
"data": {},
"error": {
"$ref": "#/definitions/publicerror.Error"
},
"message": {
"type": "string",
"example": "SUCCEED"
@@ -11620,6 +11737,9 @@
"type": "string",
"example": "invalid parameter"
},
"public_error": {
"$ref": "#/definitions/publicerror.Error"
},
"request_id": {
"type": "string"
}
@@ -12809,20 +12929,7 @@
"type": "object",
"properties": {
"error": {
"$ref": "#/definitions/httpapi.VolcesErrorPayload"
}
}
},
"httpapi.VolcesErrorPayload": {
"type": "object",
"properties": {
"code": {
"type": "string",
"example": "invalid_parameter"
},
"message": {
"type": "string",
"example": "model is required"
"$ref": "#/definitions/publicerror.Error"
}
}
},
@@ -13323,6 +13430,41 @@
"RevisionFailed"
]
},
"publicerror.Error": {
"type": "object",
"properties": {
"action": {
"type": "string"
},
"category": {
"type": "string"
},
"code": {
"type": "string"
},
"httpStatus": {
"type": "integer"
},
"message": {
"type": "string"
},
"requestId": {
"type": "string"
},
"retryAfterSeconds": {
"type": "integer"
},
"retryable": {
"type": "boolean"
},
"taskId": {
"type": "string"
},
"version": {
"type": "string"
}
}
},
"runner.PortraitAssetCapability": {
"type": "object",
"properties": {
@@ -13835,6 +13977,9 @@
"type": "object",
"additionalProperties": {}
},
"publicError": {
"$ref": "#/definitions/publicerror.Error"
},
"remoteTaskId": {
"type": "string"
},
@@ -14771,6 +14916,15 @@
"store.FileStorageChannelInput": {
"type": "object",
"properties": {
"accessKey": {
"type": "string"
},
"accessKeyId": {
"type": "string"
},
"accessKeySecret": {
"type": "string"
},
"apiKey": {
"type": "string"
},
@@ -14800,6 +14954,12 @@
"type": "string"
}
},
"secretKey": {
"type": "string"
},
"sessionToken": {
"type": "string"
},
"status": {
"type": "string"
},
@@ -14971,6 +15131,9 @@
"type": "object",
"additionalProperties": {}
},
"publicError": {
"$ref": "#/definitions/publicerror.Error"
},
"remoteTaskId": {
"type": "string"
},
@@ -16540,6 +16703,9 @@
"providerModelName": {
"type": "string"
},
"publicError": {
"$ref": "#/definitions/publicerror.Error"
},
"queueKey": {
"type": "string"
},
+122 -12
View File
@@ -481,19 +481,41 @@ definitions:
type: object
httpapi.ErrorPayload:
properties:
action:
example: retry_after
type: string
category:
example: rate_limit
type: string
code:
example: rate_limit
type: string
httpStatus:
example: 429
type: integer
message:
example: invalid json body
type: string
param: {}
requestId:
type: string
retryAfterSeconds:
example: 2
type: integer
retryable:
example: true
type: boolean
status:
example: 400
type: integer
taskId:
type: string
type:
example: invalid_request_error
type: string
version:
example: v1
type: string
type: object
httpapi.FileStorageChannelListResponse:
properties:
@@ -502,6 +524,19 @@ definitions:
$ref: '#/definitions/store.FileStorageChannel'
type: array
type: object
httpapi.FileStorageChannelTestResponse:
properties:
deleteSucceeded:
type: boolean
durationMs:
type: integer
headSucceeded:
type: boolean
provider:
type: string
putSucceeded:
type: boolean
type: object
httpapi.FileUploadData:
properties:
assetStorage:
@@ -651,6 +686,8 @@ definitions:
example: 0
type: integer
data: {}
error:
$ref: '#/definitions/publicerror.Error'
message:
example: SUCCEED
type: string
@@ -775,6 +812,8 @@ definitions:
message:
example: invalid parameter
type: string
public_error:
$ref: '#/definitions/publicerror.Error'
request_id:
type: string
type: object
@@ -1602,16 +1641,7 @@ definitions:
httpapi.VolcesErrorEnvelope:
properties:
error:
$ref: '#/definitions/httpapi.VolcesErrorPayload'
type: object
httpapi.VolcesErrorPayload:
properties:
code:
example: invalid_parameter
type: string
message:
example: model is required
type: string
$ref: '#/definitions/publicerror.Error'
type: object
httpapi.WalletAdjustmentResponse:
properties:
@@ -1953,6 +1983,29 @@ definitions:
- RevisionActive
- RevisionSuperseded
- RevisionFailed
publicerror.Error:
properties:
action:
type: string
category:
type: string
code:
type: string
httpStatus:
type: integer
message:
type: string
requestId:
type: string
retryAfterSeconds:
type: integer
retryable:
type: boolean
taskId:
type: string
version:
type: string
type: object
runner.PortraitAssetCapability:
properties:
availablePlatformIds:
@@ -2297,6 +2350,8 @@ definitions:
pricingSnapshot:
additionalProperties: {}
type: object
publicError:
$ref: '#/definitions/publicerror.Error'
remoteTaskId:
type: string
request:
@@ -2929,6 +2984,12 @@ definitions:
type: object
store.FileStorageChannelInput:
properties:
accessKey:
type: string
accessKeyId:
type: string
accessKeySecret:
type: string
apiKey:
type: string
channelKey:
@@ -2949,6 +3010,10 @@ definitions:
items:
type: string
type: array
secretKey:
type: string
sessionToken:
type: string
status:
type: string
uploadUrl:
@@ -3064,6 +3129,8 @@ definitions:
pricingSnapshot:
additionalProperties: {}
type: object
publicError:
$ref: '#/definitions/publicerror.Error'
remoteTaskId:
type: string
request:
@@ -4125,6 +4192,8 @@ definitions:
type: string
providerModelName:
type: string
publicError:
$ref: '#/definitions/publicerror.Error'
queueKey:
type: string
requestFingerprint:
@@ -6358,7 +6427,7 @@ paths:
post:
consumes:
- application/json
description: 创建文件存储通道,当前主要用于配置 server-main OpenAPI 上传通道。
description: 创建 server-main OpenAPI、阿里云 OSS 或 S3 兼容文件存储通道。
parameters:
- description: 文件存储通道
in: body
@@ -6485,6 +6554,47 @@ paths:
summary: 更新文件存储通道
tags:
- system
/api/admin/system/file-storage/channels/{channelID}/test:
post:
description: 对指定 OSS 或 S3 通道执行隔离的 Put、Head、Delete 探针,不返回凭据或对象键。
parameters:
- description: 文件存储通道 ID
in: path
name: channelID
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/httpapi.FileStorageChannelTestResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"403":
description: Forbidden
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"404":
description: Not Found
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"503":
description: Service Unavailable
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
security:
- BearerAuth: []
summary: 测试对象存储通道
tags:
- system
/api/admin/system/file-storage/settings:
get:
description: 返回文件存储系统设置;数据库对象尚未创建时返回默认设置。
@@ -8652,7 +8762,7 @@ paths:
post:
consumes:
- multipart/form-data
description: 上传文件到配置的文件存储通道;没有启用通道时回退到本地静态上传目录。单文件最大 256MiB。
description: 上传文件到配置的对象存储通道;所有通道失败时返回标准化存储错误,不写入本机静态目录。单文件最大 256MiB。
parameters:
- description: 要上传的文件
in: formData