* feat(image): 图片参数 - 统一插件声明配置与生成合同 * docs(image): 图片参数 - 说明声明合同与验证边界 * fix(plugins): 图片声明 - 保留内联素材支持并同步制品文档 * fix(image): 参数合同 - 对齐审批意图和尺寸预检调用方 * fix(image): 调用链 - 保留公开意图并接通独立分辨率 * fix(image): 图片参数 - 补齐分辨率接线与目录价格投影
167 lines
6.8 KiB
Go
167 lines
6.8 KiB
Go
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"yingce/backend/internal/model"
|
|
"yingce/backend/internal/repository"
|
|
)
|
|
|
|
// Only user-editable image options cross the approval boundary. Targets,
|
|
// references, prompt, snapshot and generation mode remain server-authored.
|
|
type CloudAgentMediaSettings struct {
|
|
LogicalModelID string `json:"logicalModelId,omitempty"`
|
|
ChannelID string `json:"channelId,omitempty"`
|
|
ChannelModelKey string `json:"channelModelKey,omitempty"`
|
|
Size string `json:"size"`
|
|
ImageResolution string `json:"imageResolution,omitempty"`
|
|
ImageParameters string `json:"imageParameters,omitempty"`
|
|
Quality string `json:"quality"`
|
|
DurationSeconds *int `json:"durationSeconds,omitempty"`
|
|
VideoGenerateAudio *bool `json:"videoGenerateAudio,omitempty"`
|
|
VideoWatermark *bool `json:"videoWatermark,omitempty"`
|
|
VideoArkPrivateAssetUpload *bool `json:"videoArkPrivateAssetUpload,omitempty"`
|
|
}
|
|
|
|
// updateCloudAgentMediaApproval rebuilds the complete prepared admission for an
|
|
// edited approval. It is called inside MutateCloudAgent so the new prepared
|
|
// hash, resource lease set and runtime checkpoint commit atomically.
|
|
func (s *Service) updateCloudAgentMediaApproval(repo *repository.Repository, run *model.CloudAgentExecution, state *cloudAgentRuntime, settings CloudAgentMediaSettings) error {
|
|
if state == nil || state.Approval == nil {
|
|
return creationConflict("审批不存在或已过期")
|
|
}
|
|
call := state.Approval.Call
|
|
if call.Function.Name != "generate_media" {
|
|
return BadAuthRequest("当前审批不是媒体生成,不能修改生成参数")
|
|
}
|
|
var args cloudAgentMediaArgs
|
|
if err := decodeCloudAgentJSONObject(call.Function.Arguments, &args); err != nil {
|
|
return err
|
|
}
|
|
if args.Mode != "image" && args.Mode != "video" {
|
|
return BadAuthRequest("仅图片或视频生成审批支持修改模型和规格")
|
|
}
|
|
args.LogicalModelID, args.ChannelID, args.ChannelModelKey = settings.LogicalModelID, settings.ChannelID, settings.ChannelModelKey
|
|
args.Size, args.ImageResolution, args.Quality = settings.Size, settings.ImageResolution, settings.Quality
|
|
args.ImageParameters = settings.ImageParameters
|
|
if args.Mode == "video" {
|
|
if settings.DurationSeconds != nil {
|
|
if *settings.DurationSeconds < 0 {
|
|
return BadAuthRequest("视频时长不能为负数")
|
|
}
|
|
args.Duration = *settings.DurationSeconds
|
|
}
|
|
args.VideoGenerateAudio = settings.VideoGenerateAudio
|
|
args.VideoWatermark = settings.VideoWatermark
|
|
args.VideoArkPrivateAssetUpload = settings.VideoArkPrivateAssetUpload
|
|
}
|
|
if args.Mode == "image" && (settings.DurationSeconds != nil || settings.VideoGenerateAudio != nil || settings.VideoWatermark != nil || settings.VideoArkPrivateAssetUpload != nil) {
|
|
return BadAuthRequest("视频参数仅适用于视频生成审批")
|
|
}
|
|
raw, err := json.Marshal(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
call.Function.Arguments = string(raw)
|
|
req, plan, err := s.prepareCloudAgentMedia(run, state, call)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Dry admission uses the same model, ownership and option checks as task
|
|
// submission, without reserving credits or creating a generation task.
|
|
// DecideCloudAgentApproval already holds storageMu for this mutation.
|
|
preparation := &creationTaskPreparation{}
|
|
req.creationPrepare = preparation
|
|
req.callerHoldsStorageMu = true
|
|
task, err := s.CreateTask(run.UserID, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var resolved map[string]any
|
|
if err := json.Unmarshal([]byte(task.InputJSON), &resolved); err != nil {
|
|
return err
|
|
}
|
|
requested, _ := req.Input["config"].(map[string]any)
|
|
if err := validateCloudAgentResolvedMediaOptions(requested, resolved); err != nil {
|
|
return err
|
|
}
|
|
name, err := s.cloudAgentMediaModelName(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
canvas, err := repo.CanvasProjectForUser(run.UserID, state.Request.CanvasID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
doc, err := creationDocument(canvas.PayloadJSON)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
prepared, err := prepareCloudAgentMediaApproval(repo, run.UserID, doc, plan, req, task, preparation.Order)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Editing an approval changes the prepared inputs and quote, but it is still
|
|
// the same user-approved generation intent until the user submits a new
|
|
// generation from the canvas.
|
|
if previous := state.Approval.Prepared; previous != nil && previous.GenerationID != "" {
|
|
prepared.GenerationID = previous.GenerationID
|
|
prepared.Hash = creationHash(prepared)
|
|
}
|
|
if err := replaceCloudAgentPreparedLeases(repo, run.UserID, run.ID, state.Approval.ID, prepared); err != nil {
|
|
return err
|
|
}
|
|
state.Calls[state.CallIndex] = call
|
|
state.Approval.Call = call
|
|
state.Approval.CallHash = cloudAgentApprovalCallHash(call)
|
|
state.Approval.ModelName = name
|
|
state.Approval.Preview = cloudAgentMediaApprovalPreview(plan, name)
|
|
state.Approval.Prepared = prepared
|
|
return nil
|
|
}
|
|
|
|
func replaceCloudAgentPreparedLeases(repo *repository.Repository, userID, runID, approvalID string, prepared *cloudAgentPreparedMedia) error {
|
|
ids := make([]string, 0, len(prepared.ResourceSignatures))
|
|
for id := range prepared.ResourceSignatures {
|
|
ids = append(ids, id)
|
|
}
|
|
return repo.ReplaceCloudAgentResourceLeases(userID, runID, approvalID, ids, prepared.Quote.ExpiresAt)
|
|
}
|
|
|
|
func validateCloudAgentResolvedMediaOptions(requested, resolvedInput map[string]any) error {
|
|
resolvedOptions, _ := resolvedInput["capabilityOptions"].(map[string]any)
|
|
resolvedConfig, _ := resolvedInput["config"].(map[string]any)
|
|
if raw := requested["imageParameters"]; raw != nil {
|
|
var values map[string]any
|
|
if encoded, ok := raw.(string); ok {
|
|
if err := json.Unmarshal([]byte(encoded), &values); err != nil {
|
|
return creationConflict("图片参数格式无效")
|
|
}
|
|
} else {
|
|
values, _ = raw.(map[string]any)
|
|
}
|
|
for name, value := range values {
|
|
if resolvedOptions[name] != value {
|
|
return creationConflict("模型解析后的图片参数与审批内容不同")
|
|
}
|
|
}
|
|
}
|
|
for _, key := range []string{"size", "imageResolution", "videoSeconds", "vquality", "quality", "count", "videoGenerateAudio", "videoWatermark", "videoArkPrivateAssetUpload"} {
|
|
requestedValue := stringValue(requested[key])
|
|
if key == "quality" && requestedValue != "" {
|
|
if _, declared := resolvedOptions[key]; !declared && !strings.EqualFold(requestedValue, stringValue(resolvedOptions["imageResolution"])) {
|
|
return creationConflict("所选图片模型未声明质量参数,请使用模型声明的图片分辨率或质量选项")
|
|
}
|
|
}
|
|
resolved := stringValue(resolvedOptions[key])
|
|
if resolved == "" {
|
|
resolved = stringValue(resolvedConfig[key])
|
|
}
|
|
if requestedValue != "" && !strings.EqualFold(requestedValue, resolved) {
|
|
return creationConflict("模型解析后的生成规格与审批参数不同,请重新读取目录并审批")
|
|
}
|
|
}
|
|
return nil
|
|
}
|