242 lines
9.1 KiB
Go
242 lines
9.1 KiB
Go
// 生成任务的风格档案(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
|
||
}
|