package httpapi import ( "encoding/json" "net/http" "strings" "github.com/easyai/easyai-ai-gateway/apps/api/internal/runner" "github.com/easyai/easyai-ai-gateway/apps/api/internal/store" ) func easyAIAsyncMediaRequest(kind string, r *http.Request) bool { if r == nil || !asyncRequest(r) { return false } switch kind { case "images.generations", "images.edits", "images.vectorize", "videos.generations", "videos.upscales", "song.generations", "music.generations", "speech.generations", "voice.clone": return true default: return false } } func easyAITaskAcceptedResponse(task store.GatewayTask) map[string]any { resultPath := "/api/v1/ai/result/" + task.ID return map[string]any{ "status": "submitted", "task_id": task.ID, "taskId": task.ID, "query_url": resultPath, "task": task, "next": map[string]string{ "events": "/api/v1/tasks/" + task.ID + "/events", "detail": "/api/v1/tasks/" + task.ID, "result": resultPath, }, } } func easyAITaskAcceptedResponseWithExtensions(task store.GatewayTask, extensions map[string]any) map[string]any { response := cloneEasyAIMap(extensions) for key, value := range easyAITaskAcceptedResponse(task) { response[key] = value } return response } func writeEasyAIAsyncError(w http.ResponseWriter, status int, message string, details map[string]any, codes ...string) { code := "" if len(codes) > 0 { code = strings.TrimSpace(codes[0]) } errorPayload := map[string]any{ "message": message, "status": status, } if code != "" { errorPayload["code"] = code } for key, value := range details { errorPayload[key] = value } response := map[string]any{ "status": "failed", "message": message, "data": []any{}, "error": errorPayload, } if code != "" { response["code"] = code } writeJSON(w, status, response) } func easyAISynchronousTaskResponse(task store.GatewayTask, output map[string]any) map[string]any { if task.Kind != "images.vectorize" { return output } response := cloneEasyAIMap(output) response["taskId"] = task.ID response["task_id"] = task.ID response["query_url"] = "/api/v1/ai/result/" + task.ID return response } func easyAIFileUploadResponse(upload map[string]any) map[string]any { response := cloneEasyAIMap(upload) response["status"] = "success" response["data"] = cloneEasyAIMap(upload) return response } func easyAITaskResultResponse(task store.GatewayTask) map[string]any { sourceResult := cloneEasyAIMap(task.Result) cleanResult := cloneEasyAIMap(sourceResult) delete(cleanResult, "raw") delete(cleanResult, "raw_data") normalizeEasyAIInlineMediaFields(cleanResult) data := easyAITaskResultData(task, sourceResult) status := easyAITaskResultStatus(task.Status) if status == "failed" { data = []any{} } cleanResult["data"] = data cleanResult["output_content"] = data response := cloneEasyAIMap(cleanResult) response["status"] = status response["task_id"] = task.ID response["taskId"] = task.ID response["query_url"] = "/api/v1/ai/result/" + task.ID response["created"] = task.CreatedAt.UnixMilli() response["data"] = data response["output_content"] = data response["output"] = easyAIOutputURLs(data) response["result"] = cleanResult upstreamTaskID := firstNonEmpty( easyAIString(cleanResult["upstream_task_id"]), task.RemoteTaskID, ) if upstreamTaskID != "" { response["upstream_task_id"] = upstreamTaskID } if len(task.Usage) > 0 && response["usage"] == nil { response["usage"] = task.Usage } cancelState := runner.DescribeTaskCancellation(task) response["cancellable"] = cancelState.Cancellable response["submitted"] = cancelState.Submitted message := firstNonEmpty( easyAIString(cleanResult["message"]), task.ErrorMessage, task.Error, task.Message, ) if message == "" { switch status { case "submitted": message = "任务已提交,请稍后查看结果" case "process": message = "任务处理中" case "failed": message = "任务执行失败" } } if message != "" { response["message"] = message } if code := firstNonEmpty(task.ErrorCode, easyAIString(cleanResult["code"])); code != "" { response["code"] = code } return response } func easyAITaskResultStatus(status string) string { switch strings.ToLower(strings.TrimSpace(status)) { case "succeeded", "success", "completed": return "success" case "failed", "cancelled", "canceled", "rejected": return "failed" case "running", "processing", "in_progress": return "process" default: return "submitted" } } func easyAITaskResultData(task store.GatewayTask, result map[string]any) []any { for _, source := range easyAIResultSources(result) { if items := easyAIOutputArray(source); len(items) > 0 { return normalizeEasyAIOutputItems(task, items) } } if item := easyAIOutputMap(result); len(item) > 0 { normalizeEasyAIInlineMediaFields(item) if easyAIOutputURL(item) != "" || easyAIOutputHasInlineMedia(item) || (strings.EqualFold(easyAIString(item["type"]), "text") && (item["text"] != nil || item["content"] != nil)) { return normalizeEasyAIOutputItems(task, []any{item}) } } return []any{} } func easyAIResultSources(result map[string]any) []any { sources := []any{ result["data"], result["output_content"], easyAIStructuredOutput(result["content"]), result["images"], result["videos"], result["audios"], result["files"], result["output"], } if raw, ok := result["raw"].(map[string]any); ok { sources = append(sources, raw["data"], raw["output_content"], easyAIStructuredOutput(raw["content"]), raw["images"], raw["videos"], raw["audios"], raw["files"], raw["output"], ) } return sources } func easyAIStructuredOutput(value any) any { switch value.(type) { case []any, []string, map[string]any: return value default: return nil } } func easyAIOutputArray(value any) []any { switch typed := value.(type) { case []any: return typed case []string: items := make([]any, 0, len(typed)) for _, item := range typed { items = append(items, item) } return items case map[string]any, string: return []any{typed} default: return nil } } func normalizeEasyAIOutputItems(task store.GatewayTask, items []any) []any { normalized := make([]any, 0, len(items)) for _, item := range items { output := easyAIOutputMap(item) if len(output) == 0 { continue } delete(output, "raw_data") normalizeEasyAIInlineMediaFields(output) mediaURL := easyAIOutputURL(output) if mediaURL != "" { output["url"] = mediaURL } if strings.TrimSpace(easyAIString(output["type"])) == "" { if outputType := easyAIOutputType(task, output, mediaURL); outputType != "" { output["type"] = outputType } } normalized = append(normalized, output) } return normalized } func easyAIOutputMap(value any) map[string]any { switch typed := value.(type) { case map[string]any: return cloneEasyAIMap(typed) case string: trimmed := strings.TrimSpace(typed) if trimmed == "" { return nil } if strings.HasPrefix(strings.ToLower(trimmed), "data:") { return map[string]any{"url": trimmed} } if strings.HasPrefix(trimmed, "/") || strings.HasPrefix(strings.ToLower(trimmed), "http://") || strings.HasPrefix(strings.ToLower(trimmed), "https://") { return map[string]any{"url": trimmed} } return map[string]any{"type": "text", "content": trimmed} default: return nil } } func easyAIOutputURL(item map[string]any) string { for _, key := range []string{ "url", "generated_url", "generatedUrl", "output_url", "outputUrl", "download_url", "downloadUrl", "image_url", "video_url", "audio_url", } { if value := easyAINestedURL(item[key]); value != "" { return value } } return "" } func easyAINestedURL(value any) string { var candidate string switch typed := value.(type) { case string: candidate = strings.TrimSpace(typed) case map[string]any: candidate = strings.TrimSpace(easyAIString(typed["url"])) default: return "" } if strings.HasPrefix(strings.ToLower(candidate), "data:") { return "" } return candidate } func normalizeEasyAIInlineMediaFields(item map[string]any) { for _, key := range []string{ "url", "generated_url", "generatedUrl", "output_url", "outputUrl", "download_url", "downloadUrl", "image_url", "video_url", "audio_url", } { value := easyAINestedURLAllowData(item[key]) contentType, payload, ok := easyAIBase64DataURL(value) if !ok { continue } if strings.HasPrefix(contentType, "image/") { if easyAIString(item["b64_json"]) == "" { item["b64_json"] = payload } } else if easyAIString(item["content"]) == "" { item["content"] = value } if easyAIString(item["mime_type"]) == "" { item["mime_type"] = contentType } delete(item, key) } } func easyAIBase64DataURL(value string) (string, string, bool) { value = strings.TrimSpace(value) if !strings.HasPrefix(strings.ToLower(value), "data:") { return "", "", false } meta, payload, ok := strings.Cut(value, ",") if !ok || strings.TrimSpace(payload) == "" { return "", "", false } parts := strings.Split(strings.TrimSpace(meta[5:]), ";") if len(parts) < 2 || !strings.EqualFold(strings.TrimSpace(parts[len(parts)-1]), "base64") { return "", "", false } contentType := strings.ToLower(strings.TrimSpace(parts[0])) if !strings.HasPrefix(contentType, "image/") && !strings.HasPrefix(contentType, "audio/") && !strings.HasPrefix(contentType, "video/") && !strings.HasPrefix(contentType, "application/") { return "", "", false } return contentType, strings.TrimSpace(payload), true } func easyAIOutputHasInlineMedia(item map[string]any) bool { if easyAIString(item["b64_json"]) != "" { return true } content := strings.ToLower(easyAIString(item["content"])) return strings.HasPrefix(content, "data:") && strings.Contains(content, ";base64,") } func easyAINestedURLAllowData(value any) string { switch typed := value.(type) { case string: return strings.TrimSpace(typed) case map[string]any: return strings.TrimSpace(easyAIString(typed["url"])) default: return "" } } func easyAIOutputType(task store.GatewayTask, item map[string]any, mediaURL string) string { kind := strings.ToLower(strings.TrimSpace(task.Kind)) switch { case kind == "images.vectorize": format := strings.ToLower(firstNonEmpty( easyAIString(item["format"]), easyAIString(task.Request["format"]), )) if format == "svg" || format == "png" { return "image" } if format != "" { return "file" } case strings.HasPrefix(kind, "images."): return "image" case strings.HasPrefix(kind, "videos.") || kind == "video.upscale": return "video" case kind == "song.generations" || kind == "music.generations" || kind == "speech.generations": return "audio" } lowerURL := strings.ToLower(mediaURL) if index := strings.IndexAny(lowerURL, "?#"); index >= 0 { lowerURL = lowerURL[:index] } switch { case strings.HasSuffix(lowerURL, ".svg"), strings.HasSuffix(lowerURL, ".png"), strings.HasSuffix(lowerURL, ".jpg"), strings.HasSuffix(lowerURL, ".jpeg"), strings.HasSuffix(lowerURL, ".webp"), strings.HasSuffix(lowerURL, ".gif"): return "image" case strings.HasSuffix(lowerURL, ".mp4"), strings.HasSuffix(lowerURL, ".mov"), strings.HasSuffix(lowerURL, ".webm"): return "video" case strings.HasSuffix(lowerURL, ".mp3"), strings.HasSuffix(lowerURL, ".wav"), strings.HasSuffix(lowerURL, ".m4a"), strings.HasSuffix(lowerURL, ".aac"), strings.HasSuffix(lowerURL, ".flac"): return "audio" case mediaURL != "": return "file" case item["text"] != nil || item["content"] != nil: return "text" default: return "" } } func easyAIOutputURLs(data []any) []string { output := make([]string, 0, len(data)) for _, item := range data { if typed, ok := item.(map[string]any); ok { if mediaURL := easyAIOutputURL(typed); mediaURL != "" { output = append(output, mediaURL) } } } return output } func easyAIString(value any) string { typed, _ := value.(string) return strings.TrimSpace(typed) } func cloneEasyAIMap(source map[string]any) map[string]any { if len(source) == 0 { return map[string]any{} } raw, err := json.Marshal(source) if err != nil { return map[string]any{} } var result map[string]any if err := json.Unmarshal(raw, &result); err != nil { return map[string]any{} } return result }