// 生成任务的风格档案(Style Profile)解析与应用:把项目/画布风格合成进提示词与参考素材, // 并校验风格素材对所选模型可用。 package app import ( "bytes" "encoding/json" "errors" "fmt" "strings" "gorm.io/gorm" ) type styleExecutionPlanDocument struct { SchemaVersion int `json:"schemaVersion"` ProfilePresetID string `json:"profilePresetId"` ProfileRevision int `json:"profileRevision"` Mode string `json:"mode"` Model string `json:"model"` InterfaceType string `json:"interfaceType"` Status string `json:"status"` Prompt string `json:"prompt"` } func (s *Service) applyGenerationStyleProfile(userID string, taskProjectID string, input *canvasGenerationInput) error { styleProfileJSON := metadataString(input.Metadata, "styleProfileJson") if strings.TrimSpace(styleProfileJSON) == "" { return nil } if _, err := validateStyleProfileJSON(styleProfileJSON); err != nil { return fmt.Errorf("项目画风执行配置无效:%w", err) } var profile styleProfileDocument if err := json.Unmarshal([]byte(styleProfileJSON), &profile); err != nil { return fmt.Errorf("项目画风执行配置解析失败:%w", err) } storedProfileJSON, storedPresetID, belongsToProject, err := s.taskProjectStyleProfile(userID, taskProjectID) if err != nil { return fmt.Errorf("读取项目画风失败:%w", err) } if belongsToProject { if strings.TrimSpace(storedProfileJSON) == "" { // 旧项目只有 preset ID,允许画布把该预设编译为结构化快照;仍需锁定同一预设,不能借降级路径换画风。 if strings.TrimSpace(storedPresetID) == "" || strings.TrimSpace(profile.PresetID) != strings.TrimSpace(storedPresetID) { return errors.New("项目画风已发生变化,请返回项目列表后重新打开当前项目再生成") } } else { matches, compareErr := equivalentStyleProfileJSON(styleProfileJSON, storedProfileJSON) if compareErr != nil { return errors.New("项目画风配置暂时无法读取,请在项目设置中重新保存画风后重试") } if !matches { // 项目画风可能在另一个页面更新;保存路径以服务端快照为准,生成时自动采用最新版本。 validatedStoredProfileJSON, validateErr := validateStyleProfileJSON(storedProfileJSON) if validateErr != nil || json.Unmarshal([]byte(validatedStoredProfileJSON), &profile) != nil { return errors.New("项目画风配置暂时无法读取,请在项目设置中重新保存画风后重试") } } } } // 执行计划是前端为即时预览生成的派生数据。平台模型入队后可能改选真实供应线路, // 因此后端必须以最终模型重新编译,不能要求用户手动“刷新配置”来同步内部路由。 plan, _ := decodeStyleExecutionPlan(input.Metadata["styleExecutionPlan"]) stylePrompt, expectedStatus, warnings := resolveGenerationStyleExecution(profile, input.Config.Model, firstNonEmpty(input.Config.InterfaceType, input.Config.APIFormat)) input.Prompt = reconcileGenerationStylePrompt(input.Prompt, plan.Prompt, stylePrompt) if expectedStatus == "blocked" { return fmt.Errorf("当前图片模型无法完整执行项目画风:%s。请切换图片模型,或在项目设置中停用对应画风资产", strings.Join(warnings, ";")) } return nil } func reconcileGenerationStylePrompt(prompt string, previousStylePrompt string, currentStylePrompt string) string { content := strings.TrimSpace(prompt) previous := strings.TrimSpace(previousStylePrompt) if previous != "" { previousBlock := "【项目画风执行规范】\n" + previous if strings.HasSuffix(content, previousBlock) { content = strings.TrimSpace(strings.TrimSuffix(content, previousBlock)) } } current := strings.TrimSpace(currentStylePrompt) if current == "" || strings.HasSuffix(content, "【项目画风执行规范】\n"+current) { return content } return strings.TrimSpace(content + "\n\n【项目画风执行规范】\n" + current) } func (s *Service) taskProjectStyleProfile(userID string, canvasOrProjectID string) (string, string, bool, error) { id := strings.TrimSpace(canvasOrProjectID) if id == "" { return "", "", false, nil } if canvas, err := s.repo.CanvasProjectForUser(userID, id); err == nil { if strings.TrimSpace(canvas.ProjectID) == "" { return "", "", false, nil } project, projectErr := s.repo.ProjectForUser(userID, canvas.ProjectID) if projectErr != nil { return "", "", true, projectErr } return project.StyleProfileJSON, project.StylePresetID, true, nil } else if !errors.Is(err, gorm.ErrRecordNotFound) { return "", "", false, err } project, err := s.repo.ProjectForUser(userID, id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return "", "", false, nil } return "", "", false, err } return project.StyleProfileJSON, project.StylePresetID, true, nil } func equivalentStyleProfileJSON(left string, right string) (bool, error) { var leftValue interface{} if err := json.Unmarshal([]byte(left), &leftValue); err != nil { return false, err } var rightValue interface{} if err := json.Unmarshal([]byte(right), &rightValue); err != nil { return false, err } leftCanonical, err := json.Marshal(leftValue) if err != nil { return false, err } rightCanonical, err := json.Marshal(rightValue) if err != nil { return false, err } return bytes.Equal(leftCanonical, rightCanonical), nil } func decodeStyleExecutionPlan(value interface{}) (styleExecutionPlanDocument, error) { if value == nil { return styleExecutionPlanDocument{}, errors.New("项目画风执行计划缺失") } raw, err := json.Marshal(value) if err != nil { return styleExecutionPlanDocument{}, errors.New("项目画风执行计划格式无效") } var plan styleExecutionPlanDocument if err := json.Unmarshal(raw, &plan); err != nil { return styleExecutionPlanDocument{}, errors.New("项目画风执行计划格式无效") } return plan, nil } func resolveGenerationStyleExecution(profile styleProfileDocument, generationModel string, interfaceType string) (string, string, []string) { fragments := []string{strings.TrimSpace(profile.Prompt)} if negative := strings.TrimSpace(profile.NegativePrompt); negative != "" { fragments = append(fragments, "【全局负面 Prompt】\n"+negative) } warnings := make([]string, 0) for _, asset := range profile.Assets { if asset.Enabled != nil && !*asset.Enabled { continue } if asset.Status != "validated" { reason := "资产尚未验证" if asset.Status == "unavailable" { reason = "资产当前不可用" } warnings = append(warnings, asset.Title+":"+reason) continue } if len(asset.BaseModels) > 0 && !styleAssetSupportsModel(asset.BaseModels, generationModel) { warnings = append(warnings, asset.Title+":仅兼容 "+strings.Join(asset.BaseModels, "、")) continue } switch asset.Kind { case "prompt", "template": fragments = append(fragments, strings.TrimSpace(asset.PromptFragment)) fragments = append(fragments, nonEmptyStyleProfileStrings(asset.TriggerWords)...) case "reference": warnings = append(warnings, asset.Title+":项目参考图自动注入适配器尚未启用") case "lora": warnings = append(warnings, asset.Title+":当前 "+firstNonEmpty(interfaceType, "图片")+" 协议未启用 LoRA 适配器") } } normalizedFragments := nonEmptyStyleProfileStrings(fragments) status := "ready" if len(warnings) > 0 { status = "degraded" if profile.ExecutionPolicy == "strict-assets" { status = "blocked" } } return strings.Join(normalizedFragments, "\n"), status, warnings } func styleAssetSupportsModel(baseModels []string, generationModel string) bool { for _, baseModel := range baseModels { if strings.EqualFold(strings.TrimSpace(baseModel), strings.TrimSpace(generationModel)) { return true } } return false } func (s *Service) validateResolvedImageCapability(input *canvasGenerationInput) error { fallback := DefaultImageCapabilityConfig(input.Config.InterfaceType, input.Config.Model) channelID := strings.TrimSpace(input.Config.ChannelID) if channelID == "" { if input.Config.CapabilityConfig != nil && input.Config.CapabilityConfig.Image != nil { input.ImageCapability = input.Config.CapabilityConfig.Image } else { input.ImageCapability = fallback } return validateImageTask(input.ImageCapability, *input) } item, err := s.repo.ChannelModelByKey(channelID, providerChannelModelKey(input.Config)) if err != nil { return errors.New("当前系统渠道模型未配置或已停用") } profile, err := DecodeModelCapabilityConfig(item.CapabilityConfigJSON) if err != nil { return errors.New("当前图片模型能力参数无效") } if profile != nil && profile.Image != nil { input.ImageCapability = profile.Image } else { input.ImageCapability = fallback } return validateImageTask(input.ImageCapability, *input) } func metadataStringValues(value any) map[string]string { values := map[string]string{} raw, ok := value.(map[string]interface{}) if !ok { return values } for key, item := range raw { values[key] = strings.TrimSpace(fmt.Sprint(item)) } return values }