diff --git a/relay/taskchannel/ali/adaptor.go b/relay/taskchannel/ali/adaptor.go index b64f27d..4c89fe2 100644 --- a/relay/taskchannel/ali/adaptor.go +++ b/relay/taskchannel/ali/adaptor.go @@ -44,6 +44,19 @@ type dashScopeVideoParams struct { Seed *int `json:"seed,omitempty"` } +// aliMetadata Ali DashScope metadata 参数结构体(用于 UnmarshalMetadata 映射) +type aliMetadata struct { + NegativePrompt string `json:"negative_prompt,omitempty"` + AudioURL string `json:"audio_url,omitempty"` + Resolution string `json:"resolution,omitempty"` + Ratio string `json:"ratio,omitempty"` + Size string `json:"size,omitempty"` + Duration *int `json:"duration,omitempty"` + PromptExtend *bool `json:"prompt_extend,omitempty"` + Watermark *bool `json:"watermark,omitempty"` + Seed *int `json:"seed,omitempty"` +} + // dashScopeImageRequest DashScope 图片生成请求 type dashScopeImageRequest struct { Model string `json:"model"` @@ -130,10 +143,12 @@ func (a *AliAdaptor) EstimateBilling(_ context.Context, _ *common.RelayInfo, bod if a.isVideo { // duration 影响计费 - if metadata, ok := req["metadata"].(map[string]any); ok { - if v, ok := metadata["duration"].(float64); ok && v > 0 { - ratios["duration"] = v - } + metadata := taskchannel.ExtractMetadata(req) + var meta struct { + Duration *int `json:"duration"` + } + if taskchannel.UnmarshalMetadata(metadata, &meta) == nil && meta.Duration != nil && *meta.Duration > 0 { + ratios["duration"] = float64(*meta.Duration) } if seconds, ok := req["seconds"].(string); ok { if d, err := parseInt(seconds); err == nil && d > 0 { @@ -202,55 +217,33 @@ func (a *AliAdaptor) buildVideoRequest(info *common.RelayInfo, req map[string]an } params := &dashScopeVideoParams{} - hasParams := false - if metadata, ok := req["metadata"].(map[string]any); ok { - if v, ok := metadata["negative_prompt"].(string); ok { - dsReq.Input.NegativePrompt = v - } - if v, ok := metadata["audio_url"].(string); ok { - dsReq.Input.AudioURL = v - } - if v, ok := metadata["resolution"].(string); ok { - params.Resolution = v - hasParams = true - } - if v, ok := metadata["ratio"].(string); ok { - params.Ratio = v - hasParams = true - } - if v, ok := metadata["size"].(string); ok { - params.Size = strings.ReplaceAll(v, "x", "*") - hasParams = true - } - if v, ok := metadata["duration"].(float64); ok && v > 0 { - d := int(v) - params.Duration = &d - hasParams = true - } - if v, ok := metadata["prompt_extend"].(bool); ok { - params.PromptExtend = &v - hasParams = true - } - if v, ok := metadata["watermark"].(bool); ok { - params.Watermark = &v - hasParams = true - } - if v, ok := metadata["seed"].(float64); ok { - s := int(v) - params.Seed = &s - hasParams = true - } + // 从 metadata 提取参数 + metadata := taskchannel.ExtractMetadata(req) + var meta aliMetadata + if err := taskchannel.UnmarshalMetadata(metadata, &meta); err != nil { + return nil, fmt.Errorf("parse ali metadata: %w", err) } + dsReq.Input.NegativePrompt = meta.NegativePrompt + dsReq.Input.AudioURL = meta.AudioURL + params.Resolution = meta.Resolution + params.Ratio = meta.Ratio + if meta.Size != "" { + params.Size = strings.ReplaceAll(meta.Size, "x", "*") + } + params.Duration = meta.Duration + params.PromptExtend = meta.PromptExtend + params.Watermark = meta.Watermark + params.Seed = meta.Seed if seconds, ok := req["seconds"].(string); ok { if d, err := parseInt(seconds); err == nil && d > 0 { params.Duration = &d - hasParams = true } } - if hasParams { + if params.Resolution != "" || params.Ratio != "" || params.Size != "" || + params.Duration != nil || params.PromptExtend != nil || params.Watermark != nil || params.Seed != nil { dsReq.Parameters = params } diff --git a/relay/taskchannel/helpers.go b/relay/taskchannel/helpers.go new file mode 100644 index 0000000..c7f0225 --- /dev/null +++ b/relay/taskchannel/helpers.go @@ -0,0 +1,52 @@ +package taskchannel + +import ( + "encoding/json" + "fmt" +) + +// ExtractMetadata 从已解析的请求 map 中提取 metadata 子 map。 +// 同时删除 "model" 字段防止通过 metadata 覆盖计费模型。 +func ExtractMetadata(req map[string]any) map[string]any { + if req == nil { + return nil + } + meta, ok := req["metadata"].(map[string]any) + if !ok { + return nil + } + delete(meta, "model") + return meta +} + +// UnmarshalMetadata 将 metadata map 通过 JSON 往返反序列化到目标结构体。 +// metadata 为 nil 时直接返回 nil,不修改 target。 +func UnmarshalMetadata(metadata map[string]any, target any) error { + if len(metadata) == 0 { + return nil + } + metaBytes, err := json.Marshal(metadata) + if err != nil { + return fmt.Errorf("marshal metadata failed: %w", err) + } + if err := json.Unmarshal(metaBytes, target); err != nil { + return fmt.Errorf("unmarshal metadata failed: %w", err) + } + return nil +} + +// DefaultString 返回 val(非空时),否则返回 fallback。 +func DefaultString(val, fallback string) string { + if val == "" { + return fallback + } + return val +} + +// DefaultInt 返回 val(非零时),否则返回 fallback。 +func DefaultInt(val, fallback int) int { + if val == 0 { + return fallback + } + return val +} diff --git a/relay/taskchannel/kling/adaptor.go b/relay/taskchannel/kling/adaptor.go index e57e241..1f86878 100644 --- a/relay/taskchannel/kling/adaptor.go +++ b/relay/taskchannel/kling/adaptor.go @@ -51,6 +51,19 @@ type klingRequest struct { CameraControl map[string]any `json:"camera_control,omitempty"` } +// klingMetadata 可灵 metadata 参数结构体(用于 UnmarshalMetadata 映射) +type klingMetadata struct { + NegativePrompt string `json:"negative_prompt,omitempty"` + Mode string `json:"mode,omitempty"` + Duration any `json:"duration,omitempty"` // string 或 number + AspectRatio string `json:"aspect_ratio,omitempty"` + CfgScale *float64 `json:"cfg_scale,omitempty"` + Image string `json:"image,omitempty"` + ImageTail string `json:"image_tail,omitempty"` + Sound string `json:"sound,omitempty"` + CameraControl map[string]any `json:"camera_control,omitempty"` +} + // klingSubmitResponse 可灵任务提交响应 type klingSubmitResponse struct { Code int `json:"code"` @@ -116,10 +129,12 @@ func (a *KlingAdaptor) EstimateBilling(_ context.Context, _ *common.RelayInfo, b } // 时长加价:10s = 2x - if metadata, ok := req["metadata"].(map[string]any); ok { - if v, ok := metadata["duration"].(float64); ok && v >= 10 { - ratios["duration_multiplier"] = 2.0 - } + metadata := taskchannel.ExtractMetadata(req) + var meta struct { + Duration float64 `json:"duration"` + } + if taskchannel.UnmarshalMetadata(metadata, &meta) == nil && meta.Duration >= 10 { + ratios["duration_multiplier"] = 2.0 } if seconds, ok := req["seconds"].(string); ok { if d, err := parseInt(seconds); err == nil && d >= 10 { @@ -143,10 +158,9 @@ func determineTaskType(req map[string]any) string { } } // 有图片输入则 image2video - if metadata, ok := req["metadata"].(map[string]any); ok { - if _, hasImg := metadata["image"]; hasImg { - return "image2video" - } + metadata := taskchannel.ExtractMetadata(req) + if _, hasImg := metadata["image"]; hasImg { + return "image2video" } if _, hasImg := req["images"].([]any); hasImg { return "image2video" @@ -204,36 +218,25 @@ func (a *KlingAdaptor) BuildRequestBody(_ context.Context, info *common.RelayInf } // 从 metadata 提取参数 - if metadata, ok := req["metadata"].(map[string]any); ok { - if v, ok := metadata["negative_prompt"].(string); ok { - kReq.NegativePrompt = v - } - if v, ok := metadata["mode"].(string); ok { - kReq.Mode = v - } - if v, ok := metadata["duration"].(string); ok { - kReq.Duration = v - } else if v, ok := metadata["duration"].(float64); ok { - kReq.Duration = fmt.Sprintf("%d", int(v)) - } - if v, ok := metadata["aspect_ratio"].(string); ok { - kReq.AspectRatio = v - } - if v, ok := metadata["cfg_scale"].(float64); ok { - kReq.CfgScale = &v - } - if v, ok := metadata["image"].(string); ok { - kReq.Image = v - } - if v, ok := metadata["image_tail"].(string); ok { - kReq.ImageTail = v - } - if v, ok := metadata["sound"].(string); ok { - kReq.Sound = v - } - if v, ok := metadata["camera_control"].(map[string]any); ok { - kReq.CameraControl = v - } + metadata := taskchannel.ExtractMetadata(req) + var meta klingMetadata + if err := taskchannel.UnmarshalMetadata(metadata, &meta); err != nil { + return nil, fmt.Errorf("parse kling metadata: %w", err) + } + kReq.NegativePrompt = meta.NegativePrompt + kReq.Mode = meta.Mode + kReq.AspectRatio = meta.AspectRatio + kReq.CfgScale = meta.CfgScale + kReq.Image = meta.Image + kReq.ImageTail = meta.ImageTail + kReq.Sound = meta.Sound + kReq.CameraControl = meta.CameraControl + // duration 支持 string 或 number 两种类型 + switch v := meta.Duration.(type) { + case string: + kReq.Duration = v + case float64: + kReq.Duration = fmt.Sprintf("%d", int(v)) } // seconds 字段映射到 duration diff --git a/relay/taskchannel/volcengine/adaptor.go b/relay/taskchannel/volcengine/adaptor.go index dbbedfd..5f59df1 100644 --- a/relay/taskchannel/volcengine/adaptor.go +++ b/relay/taskchannel/volcengine/adaptor.go @@ -48,6 +48,18 @@ type submitRequest struct { ReturnLastFrame *bool `json:"return_last_frame,omitempty"` } +// volcengineMetadata 火山引擎 metadata 参数结构体(用于 UnmarshalMetadata 映射) +type volcengineMetadata struct { + Content []contentItem `json:"content,omitempty"` + Resolution string `json:"resolution,omitempty"` + Ratio string `json:"ratio,omitempty"` + Duration *int `json:"duration,omitempty"` + Seed *int `json:"seed,omitempty"` + Watermark *bool `json:"watermark,omitempty"` + GenerateAudio *bool `json:"generate_audio,omitempty"` + ReturnLastFrame *bool `json:"return_last_frame,omitempty"` +} + // submitResponse 提交任务响应 type submitResponse struct { ID string `json:"id"` @@ -186,65 +198,20 @@ func (a *VolcengineVideoAdaptor) BuildRequestBody(_ context.Context, info *commo } // 从 metadata 中提取额外参数 - if metadata, ok := req["metadata"].(map[string]any); ok { - // content 数组中的额外项(如 video_url、audio_url) - if contentRaw, ok := metadata["content"].([]any); ok { - for _, item := range contentRaw { - if itemMap, ok := item.(map[string]any); ok { - ci := contentItem{} - if t, ok := itemMap["type"].(string); ok { - ci.Type = t - } - if t, ok := itemMap["text"].(string); ok { - ci.Text = t - } - if imgURL, ok := itemMap["image_url"].(map[string]any); ok { - if u, ok := imgURL["url"].(string); ok { - ci.ImageURL = &mediaURL{URL: u} - } - } - if vidURL, ok := itemMap["video_url"].(map[string]any); ok { - if u, ok := vidURL["url"].(string); ok { - ci.VideoURL = &mediaURL{URL: u} - } - } - if audURL, ok := itemMap["audio_url"].(map[string]any); ok { - if u, ok := audURL["url"].(string); ok { - ci.AudioURL = &mediaURL{URL: u} - } - } - if ci.Type != "" || ci.Text != "" { - volcReq.Content = append(volcReq.Content, ci) - } - } - } - } - - // 其他参数 - if v, ok := metadata["resolution"].(string); ok { - volcReq.Resolution = v - } - if v, ok := metadata["ratio"].(string); ok { - volcReq.Ratio = v - } - if v, ok := metadata["duration"].(float64); ok && v > 0 { - d := int(v) - volcReq.Duration = &d - } - if v, ok := metadata["seed"].(float64); ok { - s := int(v) - volcReq.Seed = &s - } - if v, ok := metadata["watermark"].(bool); ok { - volcReq.Watermark = &v - } - if v, ok := metadata["generate_audio"].(bool); ok { - volcReq.GenerateAudio = &v - } - if v, ok := metadata["return_last_frame"].(bool); ok { - volcReq.ReturnLastFrame = &v - } + metadata := taskchannel.ExtractMetadata(req) + var meta volcengineMetadata + if err := taskchannel.UnmarshalMetadata(metadata, &meta); err != nil { + return nil, fmt.Errorf("parse volcengine metadata: %w", err) } + // content 数组中的额外项(如 video_url、audio_url) + volcReq.Content = append(volcReq.Content, meta.Content...) + volcReq.Resolution = meta.Resolution + volcReq.Ratio = meta.Ratio + volcReq.Duration = meta.Duration + volcReq.Seed = meta.Seed + volcReq.Watermark = meta.Watermark + volcReq.GenerateAudio = meta.GenerateAudio + volcReq.ReturnLastFrame = meta.ReturnLastFrame // 从根层级提取 resolution(支持 input.resolution 格式) if v, ok := req["resolution"].(string); ok && v != "" && volcReq.Resolution == "" { @@ -410,20 +377,19 @@ func (a *VolcengineVideoAdaptor) GetChannelName() string { // hasVideoInput 检测请求是否包含视频输入 func hasVideoInput(req map[string]any) bool { - // 检查 metadata.content 中是否有 video_url 类型 - metadata, _ := req["metadata"].(map[string]any) - if metadata == nil { + metadata := taskchannel.ExtractMetadata(req) + if len(metadata) == 0 { return false } - content, _ := metadata["content"].([]any) - for _, item := range content { - if m, ok := item.(map[string]any); ok { - if m["type"] == "video_url" { - return true - } - if _, has := m["video_url"]; has { - return true - } + var meta struct { + Content []contentItem `json:"content"` + } + if taskchannel.UnmarshalMetadata(metadata, &meta) != nil { + return false + } + for _, c := range meta.Content { + if c.Type == "video_url" || c.VideoURL != nil { + return true } } return false @@ -449,10 +415,12 @@ func extractDuration(req map[string]any) int { } } // 从 metadata 提取 - if metadata, ok := req["metadata"].(map[string]any); ok { - if d, ok := metadata["duration"].(float64); ok && d > 0 { - return int(d) - } + metadata := taskchannel.ExtractMetadata(req) + var meta struct { + Duration *int `json:"duration"` + } + if taskchannel.UnmarshalMetadata(metadata, &meta) == nil && meta.Duration != nil && *meta.Duration > 0 { + return *meta.Duration } if s, ok := req["seconds"].(string); ok { if d, err := parseInt(s); err == nil && d > 0 { @@ -469,10 +437,12 @@ func extractResolution(req map[string]any) string { return r } // 从 metadata 提取 - if metadata, ok := req["metadata"].(map[string]any); ok { - if r, ok := metadata["resolution"].(string); ok && r != "" { - return r - } + metadata := taskchannel.ExtractMetadata(req) + var meta struct { + Resolution string `json:"resolution"` + } + if taskchannel.UnmarshalMetadata(metadata, &meta) == nil && meta.Resolution != "" { + return meta.Resolution } return "720p" }