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
@@ -15,6 +15,7 @@ import (
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/publicerror"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
@@ -66,17 +67,19 @@ type KelingOmniWatermarkInfo struct {
}
type KelingCompatibleEnvelope struct {
Code int `json:"code" example:"0"`
Message string `json:"message" example:"SUCCEED"`
RequestID string `json:"request_id"`
Data any `json:"data,omitempty"`
Code int `json:"code" example:"0"`
Message string `json:"message" example:"SUCCEED"`
RequestID string `json:"request_id"`
Data any `json:"data,omitempty"`
Error *publicerror.Error `json:"error,omitempty"`
}
type kelingCompatError struct {
HTTPStatus int
Code int
Message string
RequestID string
HTTPStatus int
Code int
Message string
RequestID string
PublicError *publicerror.Error
}
func (e *kelingCompatError) Error() string {
@@ -200,10 +203,6 @@ func (s *Server) createKelingOmniVideo(w http.ResponseWriter, r *http.Request) {
}
task, createErr = s.waitForCompatibilitySubmission(r, task)
if createErr != nil {
if wire := clients.ErrorWireResponse(createErr); wireResponseMatches(wire, clients.ProtocolKlingV1Omni) {
writeWireResponse(w, wire)
return
}
writeKelingCompatError(w, requestID, kelingCompatGatewayError(createErr))
return
}
@@ -690,11 +689,13 @@ func kelingCompatTaskData(task store.GatewayTask) map[string]any {
"enabled": kelingCompatTaskWatermark(task.Request),
},
}
if message := kelingCompatTaskMessage(task); message != "" {
data["task_status_msg"] = message
}
if kelingCompatTaskStatus(task.Status) == "failed" {
data["task_status_code"] = kelingCompatBusinessCode(task.ErrorCode, kelingCompatTaskMessage(task))
standard := publicTaskError(task)
data["task_status_msg"] = standard.Message
data["task_status_code"] = kelingCompatBusinessCode(standard.Code, standard.Message)
data["error"] = standard
} else if message := kelingCompatTaskMessage(task); message != "" {
data["task_status_msg"] = message
}
if videos := kelingCompatTaskVideos(task.Result); len(videos) > 0 {
data["task_result"] = map[string]any{"videos": videos}
@@ -770,6 +771,9 @@ func firstKelingCompatValue(values ...any) any {
}
func kelingCompatTaskMessage(task store.GatewayTask) string {
if task.Status == "failed" || task.Status == "cancelled" || task.ErrorCode != "" || task.ErrorMessage != "" || task.Error != "" {
return publicTaskError(task).Message
}
return strings.TrimSpace(firstNonEmpty(task.ErrorMessage, task.Error, task.Message))
}
@@ -825,8 +829,11 @@ func kelingCompatGatewayError(err error) *kelingCompatError {
return newKelingCompatError(http.StatusInternalServerError, 5000, "unknown gateway error")
}
codeText := clients.ErrorCode(err)
businessCode := kelingCompatBusinessCode(codeText, err.Error())
status := http.StatusInternalServerError
status := statusFromRunError(err)
standard := publicerror.FromFields(codeText, err.Error(), status, clients.IsRetryable(err))
publicerror.Observe(standard)
businessCode := kelingCompatBusinessCode(standard.Code, standard.Message)
status = standard.HTTPStatus
switch businessCode {
case 1101:
status = http.StatusPaymentRequired
@@ -841,7 +848,9 @@ func kelingCompatGatewayError(err error) *kelingCompatError {
case 5001:
status = http.StatusBadGateway
}
return newKelingCompatError(status, businessCode, err.Error())
result := newKelingCompatError(status, businessCode, standard.Message)
result.PublicError = &standard
return result
}
func kelingCompatBusinessCode(errorCode string, message string) int {
@@ -923,9 +932,18 @@ func writeKelingCompatError(w http.ResponseWriter, requestID string, err *keling
if status == 0 {
status = http.StatusInternalServerError
}
standard := err.PublicError
if standard == nil {
value := publicerror.FromFields("", err.Message, status, status == http.StatusRequestTimeout || status == http.StatusTooManyRequests || status >= 500)
value = publicerror.WithIDs(value, requestID, "")
publicerror.Observe(value)
standard = &value
}
status = standard.HTTPStatus
writeJSON(w, status, KelingCompatibleEnvelope{
Code: err.Code,
Message: err.Message,
Message: standard.Message,
RequestID: requestID,
Error: standard,
})
}