Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_generation_contract.go
huiku 7466119867 feat(image): 以插件声明统一图片参数、尺寸选择与定价合同 (#760)
* feat(image): 图片参数 - 统一插件声明配置与生成合同

* docs(image): 图片参数 - 说明声明合同与验证边界

* fix(plugins): 图片声明 - 保留内联素材支持并同步制品文档

* fix(image): 参数合同 - 对齐审批意图和尺寸预检调用方

* fix(image): 调用链 - 保留公开意图并接通独立分辨率

* fix(image): 图片参数 - 补齐分辨率接线与目录价格投影
2026-10-13 19:18:21 +08:00

99 lines
4.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"strings"
"yingce/backend/internal/canvas/contract"
)
// cloudAgentGenerationSpec 将 Agent 参数和已解析素材转换为画布生成合同。
// refs 提供真实素材类型,resolvedConfig 提供准入后的模型默认值;返回可保存的规格或参数错误。
func cloudAgentGenerationSpec(a cloudAgentMediaArgs, refs map[string]any, resolvedConfig map[string]any) (contract.GenerationSpec, error) {
selection := &contract.ModelSelection{Kind: "channel", ChannelID: a.ChannelID, ModelKey: a.ChannelModelKey}
if a.LogicalModelID != "" {
selection = &contract.ModelSelection{Kind: "logical", LogicalModelID: a.LogicalModelID}
}
options := contract.Options{}
if a.Mode == "image" || a.Mode == "video" {
// A blank option means "use the selected model's declared default". Do
// not turn absence into an explicit empty value: task admission is where
// the model capability contract resolves defaults and validates support.
if strings.TrimSpace(a.Size) != "" {
options.Size = &a.Size
}
}
if a.Mode == "video" {
if a.Duration > 0 {
options.DurationSeconds = &a.Duration
}
options.GenerateAudio = a.VideoGenerateAudio
options.Watermark = a.VideoWatermark
if a.Quality != "" {
options.Resolution = &a.Quality
}
} else if a.Mode == "image" {
count := 1
options.Count = &count
if a.ImageResolution != "" {
options.ImageResolution = &a.ImageResolution
}
if a.ImageParameters != "" {
options.ImageParameters = &a.ImageParameters
}
if a.Quality != "" {
options.Quality = &a.Quality
}
}
if resolvedConfig != nil {
config := options.TaskConfig()
for key, value := range resolvedConfig {
config[key] = value
}
var err error
options, err = contract.OptionsFromTaskConfig(a.Mode, config)
if err != nil {
return contract.GenerationSpec{}, err
}
}
byID := map[string]contract.ReferenceBinding{}
for _, media := range []struct{ field, kind string }{{"referenceImages", "image"}, {"referenceVideos", "video"}, {"referenceAudios", "audio"}} {
for _, ref := range creationMaps(refs[media.field]) {
id := stringValue(ref["id"])
binding := contract.ReferenceBinding{ID: "node:" + id, NodeID: id, MediaType: media.kind, Role: "reference", Resolution: "latest"}
if key := stringValue(ref["storageKey"]); strings.HasPrefix(key, "resource:") {
binding.ResourceID = strings.TrimPrefix(key, "resource:")
}
byID[id] = binding
}
}
bindings := []contract.ReferenceBinding{}
// 帧选择沿用视频编辑器的 metadata 字段;这里只校验真实引用,不从提示词或图片顺序猜用途。
for _, frame := range []struct{ field, id string }{
{"videoStartFrameNodeId", a.VideoStartFrameNodeID},
{"videoEndFrameNodeId", a.VideoEndFrameNodeID},
} {
if frame.id == "" {
continue
}
binding, exists := byID[frame.id]
if a.Mode != "video" || !exists || binding.MediaType != "image" || !containsString(a.ReferenceNodeIDs, frame.id) {
return contract.GenerationSpec{}, cloudAgentFieldError(frame.field, "invalid_value", "首尾帧仅用于视频,必须选择 referenceNodeIds 中的真实图片节点")
}
}
for _, id := range a.ReferenceNodeIDs {
binding, exists := byID[id]
if !exists {
return contract.GenerationSpec{}, BadAuthRequest("生成引用未解析,不能保存不完整生成规格")
}
binding.Order = len(bindings)
bindings = append(bindings, binding)
}
for _, id := range a.ReferenceTransientIDs {
bindings = append(bindings, contract.ReferenceBinding{ID: "transient:" + id, TransientID: id, MediaType: "image", Role: "reference", Order: len(bindings), Resolution: "snapshot"})
}
if a.SourceNodeID != "" {
bindings = append(bindings, contract.ReferenceBinding{ID: "node:" + a.SourceNodeID, NodeID: a.SourceNodeID, MediaType: "text", Role: "source-text", Order: len(bindings), Resolution: "latest"})
}
spec := contract.GenerationSpec{Version: contract.GenerationVersion, Mode: a.Mode, Prompt: a.Prompt, ModelSelection: selection, Options: options, ReferenceBindings: bindings, TextInputMode: "prompt-only"}
return spec, spec.Validate()
}