fix(media): 对齐 EasyAI 媒体响应与转存策略
统一媒体异步提交和轮询响应,扩展 /ai/result 到当前用户的 Gateway 媒体任务,并保持既有 Gateway 字段兼容。\n\n启用转存但无可用渠道时回退到 24 小时本地静态资源;关闭转存时保留图片、音频等上游 Base64 字段。同步更新文件上传兼容结构、OpenAPI、管理端说明和回归测试。\n\n验证:cd apps/api && env -u AI_GATEWAY_TEST_DATABASE_URL go test ./... -count=1;gofmt -l 无输出;pnpm nx run web:typecheck。
This commit is contained in:
@@ -1039,15 +1039,14 @@ func (s *Server) listModelRateLimitStatuses(w http.ResponseWriter, r *http.Reque
|
||||
// @Failure 429 {object} ErrorEnvelope
|
||||
// @Failure 502 {object} ErrorEnvelope
|
||||
// @Router /api/v1/reranks [post]
|
||||
// @Router /api/v1/videos/generations [post]
|
||||
// @Router /api/v1/song/generations [post]
|
||||
// @Router /api/v1/music/generations [post]
|
||||
// @Router /api/v1/speech/generations [post]
|
||||
// @Router /api/v1/voice_clone [post]
|
||||
func (s *Server) createTask(kind string, compatible bool) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
targetProtocol := targetProtocolForTaskRequest(kind, r)
|
||||
writeTaskError := func(status int, message string, details map[string]any, codes ...string) {
|
||||
if easyAIAsyncMediaRequest(kind, r) {
|
||||
writeEasyAIAsyncError(w, status, message, details, codes...)
|
||||
return
|
||||
}
|
||||
if targetProtocol == "" {
|
||||
writeErrorWithDetails(w, status, message, details, codes...)
|
||||
return
|
||||
@@ -1093,9 +1092,10 @@ func (s *Server) createTask(kind string, compatible bool) http.Handler {
|
||||
return
|
||||
}
|
||||
responsePlan := planTaskResponse(kind, compatible, body, r)
|
||||
if targetProtocol != "" {
|
||||
if targetProtocol != "" && !easyAIAsyncMediaRequest(kind, r) {
|
||||
// OpenAI compatibility endpoints are synchronous APIs. Gateway async
|
||||
// task envelopes belong only to native Gateway routes.
|
||||
// task envelopes belong only to native Gateway routes, except for the
|
||||
// explicit EasyAI media X-Async extension.
|
||||
responsePlan.asyncMode = false
|
||||
}
|
||||
idempotencyKey, hasIdempotencyKey, err := optionalTaskIdempotencyKey(r)
|
||||
@@ -1150,7 +1150,7 @@ func (s *Server) createTask(kind string, compatible bool) http.Handler {
|
||||
writeTaskError(http.StatusConflict, "streaming idempotent replay is not supported", nil, "idempotency_stream_replay_unsupported")
|
||||
return
|
||||
}
|
||||
if targetProtocol != "" {
|
||||
if targetProtocol != "" && !responsePlan.asyncMode {
|
||||
if task.Status == "succeeded" {
|
||||
writeJSON(w, http.StatusOK, task.Result)
|
||||
return
|
||||
@@ -1274,7 +1274,7 @@ func openAIEmbeddingsDoc() {}
|
||||
// @Security BearerAuth
|
||||
// @Param input body TaskRequest true "OpenAI Images 请求"
|
||||
// @Param X-Async header bool false "true 时异步创建任务并返回 202"
|
||||
// @Success 200 {object} CompatibleResponse
|
||||
// @Success 200 {object} OpenAIImagesResponse
|
||||
// @Success 202 {object} TaskAcceptedResponse
|
||||
// @Failure 400 {object} OpenAIErrorEnvelope
|
||||
// @Failure 401 {object} OpenAIErrorEnvelope
|
||||
@@ -1284,6 +1284,32 @@ func openAIEmbeddingsDoc() {}
|
||||
// @Router /api/v1/images/edits [post]
|
||||
func openAIImagesDoc() {}
|
||||
|
||||
// easyAIMediaTasksDoc godoc
|
||||
// @Summary 创建 EasyAI 媒体任务
|
||||
// @Description 默认同步返回 EasyAI GeneratedResponse;设置 X-Async=true 时返回同时兼容 Gateway 与 server-main EasyAIClient 的异步提交结构。
|
||||
// @Tags media
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param X-Async header bool false "true 时异步创建任务并返回 202"
|
||||
// @Param Idempotency-Key header string false "可选请求幂等键;同一用户范围内唯一"
|
||||
// @Param input body TaskRequest true "媒体任务请求,字段随任务类型变化"
|
||||
// @Success 200 {object} EasyAIGeneratedResponse
|
||||
// @Success 202 {object} TaskAcceptedResponse
|
||||
// @Failure 400 {object} ErrorEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 402 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 404 {object} ErrorEnvelope
|
||||
// @Failure 429 {object} ErrorEnvelope
|
||||
// @Failure 502 {object} ErrorEnvelope
|
||||
// @Router /api/v1/videos/generations [post]
|
||||
// @Router /api/v1/song/generations [post]
|
||||
// @Router /api/v1/music/generations [post]
|
||||
// @Router /api/v1/speech/generations [post]
|
||||
// @Router /api/v1/voice_clone [post]
|
||||
func easyAIMediaTasksDoc() {}
|
||||
|
||||
func (s *Server) requestExecutionContext(r *http.Request) (context.Context, context.CancelFunc) {
|
||||
base := context.WithoutCancel(r.Context())
|
||||
if s.ctx == nil {
|
||||
@@ -1418,7 +1444,7 @@ func writeProtocolCompatibleTaskResponse(runCtx context.Context, w http.Response
|
||||
writeWireResponse(w, result.Wire)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result.Output)
|
||||
writeJSON(w, http.StatusOK, easyAISynchronousTaskResponse(result.Task, result.Output))
|
||||
}
|
||||
|
||||
func streamIncludeUsage(body map[string]any) bool {
|
||||
@@ -1461,14 +1487,7 @@ func streamCompatibleKind(kind string) bool {
|
||||
}
|
||||
|
||||
func writeTaskAccepted(w http.ResponseWriter, task store.GatewayTask) {
|
||||
writeJSON(w, http.StatusAccepted, map[string]any{
|
||||
"taskId": task.ID,
|
||||
"task": task,
|
||||
"next": map[string]string{
|
||||
"events": fmt.Sprintf("/api/v1/tasks/%s/events", task.ID),
|
||||
"detail": fmt.Sprintf("/api/v1/tasks/%s", task.ID),
|
||||
},
|
||||
})
|
||||
writeJSON(w, http.StatusAccepted, easyAITaskAcceptedResponse(task))
|
||||
}
|
||||
|
||||
func apiKeyScopeAllowed(user *auth.User, kind string) bool {
|
||||
|
||||
Reference in New Issue
Block a user