Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_generation_contract.go
T
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

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()
}