package controller import ( "bytes" "fmt" "io" "net/http" "strconv" "strings" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/middleware" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/relay" klingaiping "github.com/QuantumNous/new-api/relay/channel/task/kling/aiping" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) func KlingAipingNativeTaskSubmit(c *gin.Context) { route, ok := klingaiping.FindRoute(c.Request.Method, c.FullPath(), klingaiping.RouteKindSubmit) if !ok { c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "route not found"}) return } relayInfo, err := relaycommon.GenRelayInfo(c, types.RelayFormatTask, nil, nil) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()}) return } payload, err := readJSONPayload(c) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()}) return } configureKlingAipingTaskRelayInfo(c, relayInfo, route, payload) if bound := resolveKlingAipingBoundChannel(c, relayInfo); bound != nil { relayInfo.LockedChannel = bound } relayTaskWithInfo(c, relayInfo) } func resolveKlingAipingBoundChannel(c *gin.Context, relayInfo *relaycommon.RelayInfo) *model.Channel { group := concreteTaskVideoBindingGroup(c, relayInfo.TokenGroup) if group == "" { return nil } channel, err := service.GetBoundKlingAssetChannelForModel(c.GetInt("id"), group, relayInfo.OriginModelName) if err != nil { logger.LogError(c, fmt.Sprintf("resolve kling aiping bound channel failed: %v", err)) return nil } if channel == nil || !service.IsUsableKlingAssetChannel(channel, group) { return nil } if !model.IsChannelEnabledForGroupModel(group, relayInfo.OriginModelName, channel.Id) { return nil } return channel } func configureKlingAipingTaskRelayInfo(c *gin.Context, relayInfo *relaycommon.RelayInfo, route klingaiping.Route, payload map[string]any) { // Native Kling routes bypass Distribute(), so force relayTaskWithInfo to // select a channel instead of reading a preselected one from context. if relayInfo.ChannelMeta == nil { relayInfo.ChannelMeta = &relaycommon.ChannelMeta{} } modelName := resolveKlingAipingModel(payload, route) relayInfo.OriginModelName = modelName relayInfo.Action = route.Action relaycommon.StoreTaskRequest(c, relayInfo, route.Action, relaycommon.TaskSubmitReq{ Model: modelName, Prompt: stringFromMap(payload, "prompt"), Seconds: fmt.Sprint(payload["seconds"]), Metadata: payload, }) } func KlingAipingNativeTaskFetch(c *gin.Context) { route, ok := klingaiping.FindRoute(c.Request.Method, c.FullPath(), klingaiping.RouteKindFetch) if !ok { c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "route not found"}) return } task, ok := getKlingAipingUserTask(c, c.Param("task_id"), route.Action) if !ok { return } refreshKlingAipingTaskIfNeeded(task) c.JSON(http.StatusOK, buildKlingAipingTaskPayload(task)) } func KlingAipingNativeTaskList(c *gin.Context) { route, ok := klingaiping.FindRoute(c.Request.Method, c.FullPath(), klingaiping.RouteKindList) if !ok { c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "route not found"}) return } pageNum, pageSize, err := parseKlingAipingPage(c) if err != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"code": 422, "message": err.Error()}) return } tasks := model.TaskGetAllUserTask(c.GetInt("id"), (pageNum-1)*pageSize, pageSize, model.SyncTaskQueryParams{ Platform: constant.TaskPlatform(strconv.Itoa(constant.ChannelTypeKlingAiping)), Action: route.Action, }) data := make([]any, 0, len(tasks)) for _, task := range tasks { data = append(data, taskDataObject(task)) } c.JSON(http.StatusOK, gin.H{ "code": 0, "message": "success", "request_id": c.GetString(common.RequestIdKey), "data": data, }) } func KlingAipingNativeProxy(c *gin.Context) { route, ok := klingaiping.FindRoute(c.Request.Method, c.FullPath(), klingaiping.RouteKindProxy) if !ok { c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "route not found"}) return } group := common.GetContextKeyString(c, constant.ContextKeyUsingGroup) channel := resolveKlingAipingBoundChannelForProxy(c, group, route.BillingModel) if channel == nil { var err error channel, _, err = service.CacheGetRandomSatisfiedChannel(&service.RetryParam{ Ctx: c, TokenGroup: group, ModelName: route.BillingModel, Retry: common.GetPointer(0), AllowedChannelTypes: service.VideoAssetChannelTypesForFamily(service.VideoAssetFamilyKling), }) if err != nil { c.JSON(http.StatusServiceUnavailable, gin.H{"code": 503, "message": err.Error()}) return } } if setupErr := middleware.SetupContextForSelectedChannel(c, channel, route.BillingModel); setupErr != nil { c.JSON(setupErr.StatusCode, gin.H{"code": setupErr.GetErrorCode(), "message": setupErr.Error()}) return } resp, err := doKlingAipingProxyRequest(c, route, channel) if err != nil { c.JSON(http.StatusBadGateway, gin.H{"code": 502, "message": err.Error()}) return } defer resp.Body.Close() copyProxyResponse(c, resp) } func resolveKlingAipingBoundChannelForProxy(c *gin.Context, group, billingModel string) *model.Channel { group = strings.TrimSpace(group) if group == "" || group == "auto" || strings.TrimSpace(billingModel) == "" { return nil } channel, err := service.GetBoundKlingAssetChannelForModel(c.GetInt("id"), group, billingModel) if err != nil { logger.LogError(c, fmt.Sprintf("resolve kling aiping bound channel (proxy) failed: %v", err)) return nil } if channel == nil || !service.IsUsableKlingAssetChannel(channel, group) { return nil } if !model.IsChannelEnabledForGroupModel(group, billingModel, channel.Id) { return nil } return channel } func readJSONPayload(c *gin.Context) (map[string]any, error) { body, err := common.GetBodyStorage(c) if err != nil { return nil, err } data, err := body.Bytes() if err != nil { return nil, err } _, _ = body.Seek(0, io.SeekStart) c.Request.Body = io.NopCloser(body) payload := map[string]any{} if strings.TrimSpace(string(data)) == "" { return payload, nil } if err := common.Unmarshal(data, &payload); err != nil { return nil, err } return payload, nil } func resolveKlingAipingModel(payload map[string]any, route klingaiping.Route) string { if modelName := stringFromMap(payload, "model_name"); modelName != "" { return modelName } if modelName := stringFromMap(payload, "model"); modelName != "" { return modelName } if route.BillingModel != "" { return route.BillingModel } return "kling-v3" } func stringFromMap(payload map[string]any, key string) string { if value, ok := payload[key].(string); ok { return strings.TrimSpace(value) } return "" } func getKlingAipingUserTask(c *gin.Context, taskID string, action string) (*model.Task, bool) { task, exist, err := model.GetByTaskId(c.GetInt("id"), taskID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": err.Error()}) return nil, false } if !exist || task.Platform != constant.TaskPlatform(strconv.Itoa(constant.ChannelTypeKlingAiping)) || task.Action != action { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "task not found"}) return nil, false } return task, true } func refreshKlingAipingTaskIfNeeded(task *model.Task) { if task.Status == model.TaskStatusSuccess || task.Status == model.TaskStatusFailure { return } channelModel, err := model.GetChannelById(task.ChannelId, true) if err != nil || channelModel == nil { return } adaptor := relay.GetTaskAdaptor(task.Platform) if adaptor == nil { return } resp, err := adaptor.FetchTask(channelModel.GetBaseURL(), channelModel.Key, map[string]any{ "task_id": task.GetUpstreamTaskID(), "action": task.Action, }, channelModel.GetSetting().Proxy) if err != nil || resp == nil { return } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil || resp.StatusCode < 200 || resp.StatusCode >= 300 { return } taskInfo, err := adaptor.ParseTaskResult(body) if err != nil || taskInfo == nil { return } snap := task.Snapshot() task.Data = body if taskInfo.Status != "" { task.Status = model.TaskStatus(taskInfo.Status) } if taskInfo.Progress != "" { task.Progress = taskInfo.Progress } if taskInfo.Url != "" { task.PrivateData.ResultURL = taskInfo.Url } if !snap.Equal(task.Snapshot()) { _, _ = task.UpdateWithStatus(snap.Status) } } func buildKlingAipingTaskPayload(task *model.Task) map[string]any { payload := map[string]any{ "code": 0, "message": "success", "data": taskDataObject(task), } return payload } func taskDataObject(task *model.Task) map[string]any { payload := map[string]any{} _ = common.Unmarshal(task.Data, &payload) delete(payload, "aiping_id") data, _ := payload["data"].(map[string]any) if data == nil { data = map[string]any{} } data["task_id"] = task.TaskID if _, ok := data["task_status"]; !ok { data["task_status"] = mapKlingAipingTaskStatus(task.Status) } if _, ok := data["task_status_msg"]; !ok { data["task_status_msg"] = task.FailReason } if _, ok := data["created_at"]; !ok && task.CreatedAt != 0 { data["created_at"] = task.CreatedAt } if _, ok := data["updated_at"]; !ok && task.UpdatedAt != 0 { data["updated_at"] = task.UpdatedAt } ensureKlingAipingWatermarkURL(data) return data } func mapKlingAipingTaskStatus(status model.TaskStatus) string { switch status { case model.TaskStatusSubmitted, model.TaskStatusQueued: return "submitted" case model.TaskStatusInProgress: return "processing" case model.TaskStatusSuccess: return "succeed" case model.TaskStatusFailure: return "failed" default: return "processing" } } func ensureKlingAipingWatermarkURL(data map[string]any) { taskResult, _ := data["task_result"].(map[string]any) if taskResult == nil { return } videos, _ := taskResult["videos"].([]any) for _, videoAny := range videos { video, _ := videoAny.(map[string]any) if video == nil { continue } if _, ok := video["watermark_url"]; !ok { video["watermark_url"] = "" } } } func parseKlingAipingPage(c *gin.Context) (int, int, error) { pageNum := parseIntDefault(c.Query("pageNum"), 1) pageSize := parseIntDefault(c.Query("pageSize"), 30) if pageNum < 1 || pageNum > 1000 { return 0, 0, fmt.Errorf("pageNum must be in [1, 1000]") } if pageSize < 1 || pageSize > 500 { return 0, 0, fmt.Errorf("pageSize must be in [1, 500]") } return pageNum, pageSize, nil } func parseIntDefault(raw string, fallback int) int { if strings.TrimSpace(raw) == "" { return fallback } v, err := strconv.Atoi(raw) if err != nil { return -1 } return v } func doKlingAipingProxyRequest(c *gin.Context, route klingaiping.Route, channelModel *model.Channel) (*http.Response, error) { baseURL := strings.TrimRight(channelModel.GetBaseURL(), "/") upstreamPath := strings.Replace(route.UpstreamPath, ":id", c.Param("id"), 1) url := baseURL + upstreamPath if c.Request.URL.RawQuery != "" { url += "?" + c.Request.URL.RawQuery } var body io.Reader if c.Request.Method != http.MethodGet { data, err := proxyBodyBytes(c, route) if err != nil { return nil, err } body = bytes.NewReader(data) } req, err := http.NewRequest(c.Request.Method, url, body) if err != nil { return nil, err } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") key := common.GetContextKeyString(c, constant.ContextKeyChannelKey) if key == "" { key = channelModel.Key } req.Header.Set("Authorization", "Bearer "+key) client, err := service.GetHttpClientWithProxy(channelModel.GetSetting().Proxy) if err != nil { return nil, err } return client.Do(req) } func proxyBodyBytes(c *gin.Context, route klingaiping.Route) ([]byte, error) { storage, err := common.GetBodyStorage(c) if err != nil { return nil, err } return storage.Bytes() } func copyProxyResponse(c *gin.Context, resp *http.Response) { if resp.StatusCode >= http.StatusBadRequest { copyNormalizedProxyError(c, resp) return } for key, values := range resp.Header { for _, value := range values { c.Writer.Header().Add(key, value) } } c.Status(resp.StatusCode) _, _ = io.Copy(c.Writer, resp.Body) } func copyNormalizedProxyError(c *gin.Context, resp *http.Response) { body, _ := io.ReadAll(resp.Body) message := strings.TrimSpace(string(body)) payload := map[string]any{} if len(body) > 0 && common.Unmarshal(body, &payload) == nil { if msg := stringFromMap(payload, "message"); msg != "" { message = msg } else if msg := stringFromMap(payload, "msg"); msg != "" { message = msg } else if detail, ok := payload["detail"]; ok { if detailMap, ok := detail.(map[string]any); ok { if msg := stringFromMap(detailMap, "message"); msg != "" { message = msg } else if msg := stringFromMap(detailMap, "msg"); msg != "" { message = msg } else { message = fmt.Sprint(detail) } } else { message = fmt.Sprint(detail) } } delete(payload, "msg") } else { payload = map[string]any{} } if message == "" { message = resp.Status } if _, ok := payload["code"]; !ok { payload["code"] = resp.StatusCode } payload["message"] = message payload["request_id"] = c.GetString(common.RequestIdKey) c.JSON(resp.StatusCode, payload) } func normalizeKlingAipingTaskError(taskErr *dto.TaskError) { if taskErr == nil || strings.TrimSpace(taskErr.Message) == "" { return } payload := map[string]any{} if common.Unmarshal([]byte(taskErr.Message), &payload) != nil { return } if msg := stringFromMap(payload, "message"); msg != "" { taskErr.Message = msg return } if msg := stringFromMap(payload, "msg"); msg != "" { taskErr.Message = msg return } if detail, ok := payload["detail"].(map[string]any); ok { if msg := stringFromMap(detail, "message"); msg != "" { taskErr.Message = msg } } }