Files

183 lines
6.6 KiB
Go

package workbench
import (
"encoding/json"
"fmt"
"strings"
)
// InputContract describes the media and text inputs a workbench tool expects.
// It is stored inside capability JSON so the catalog and task admission share one contract.
type InputContract struct {
PromptRequired bool `json:"promptRequired"`
MinTextReferences int `json:"minTextReferences"`
Slots []ReferenceSlot `json:"slots"`
}
type ReferenceSlot struct {
Key string `json:"key"`
Name string `json:"name"`
MediaTypes []string `json:"mediaTypes"`
ResourceKind string `json:"resourceKind"`
MaxCount int `json:"maxCount"`
Required bool `json:"required"`
MinCount int `json:"minCount"`
CoverResourceID string `json:"coverResourceId,omitempty"`
SampleResourceID string `json:"sampleResourceId,omitempty"`
}
type ResultExample struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
ImageResourceIDs []string `json:"imageResourceIds,omitempty"`
VideoResourceID string `json:"videoResourceId,omitempty"`
}
type catalogConfig struct {
InputContract InputContract `json:"inputContract"`
SearchKeywords []string `json:"searchKeywords,omitempty"`
Surfaces []string `json:"surfaces,omitempty"`
Recommended bool `json:"recommended"`
RecommendationOrder int `json:"recommendationOrder"`
InputHint string `json:"inputHint,omitempty"`
ResultExample ResultExample `json:"resultExample,omitempty"`
}
func parseCatalogConfig(capabilityJSON, guideJSON string) (catalogConfig, error) {
config := catalogConfig{InputContract: InputContract{PromptRequired: true, Slots: []ReferenceSlot{}}}
capability := map[string]json.RawMessage{}
if strings.TrimSpace(capabilityJSON) != "" && strings.TrimSpace(capabilityJSON) != "{}" {
if err := json.Unmarshal([]byte(capabilityJSON), &capability); err != nil {
return config, fmt.Errorf("capability: %w", err)
}
}
if raw, ok := capability["inputContract"]; ok {
if err := json.Unmarshal(raw, &config.InputContract); err != nil {
return config, fmt.Errorf("inputContract: %w", err)
}
}
if raw, ok := capability["searchKeywords"]; ok {
_ = json.Unmarshal(raw, &config.SearchKeywords)
}
if raw, ok := capability["surfaces"]; ok {
_ = json.Unmarshal(raw, &config.Surfaces)
}
if raw, ok := capability["recommended"]; ok {
_ = json.Unmarshal(raw, &config.Recommended)
}
if raw, ok := capability["recommendationOrder"]; ok {
_ = json.Unmarshal(raw, &config.RecommendationOrder)
}
guide := map[string]json.RawMessage{}
if strings.TrimSpace(guideJSON) != "" && strings.TrimSpace(guideJSON) != "{}" {
if err := json.Unmarshal([]byte(guideJSON), &guide); err != nil {
return config, fmt.Errorf("guide: %w", err)
}
}
if raw, ok := guide["inputHint"]; ok {
_ = json.Unmarshal(raw, &config.InputHint)
}
if raw, ok := guide["resultExample"]; ok {
if err := json.Unmarshal(raw, &config.ResultExample); err != nil {
return config, fmt.Errorf("resultExample: %w", err)
}
}
return config, nil
}
func normalizeCatalogConfig(config catalogConfig) (catalogConfig, error) {
if config.InputContract.MinTextReferences < 0 || config.InputContract.MinTextReferences > 20 {
return config, fmt.Errorf("文本参考数量无效")
}
if len(config.InputContract.Slots) > 20 {
return config, fmt.Errorf("输入素材槽位不能超过 20 个")
}
seen := map[string]bool{}
for index := range config.InputContract.Slots {
slot := &config.InputContract.Slots[index]
slot.Key = strings.TrimSpace(slot.Key)
slot.Name = strings.TrimSpace(slot.Name)
slot.ResourceKind = strings.TrimSpace(slot.ResourceKind)
if slot.ResourceKind == "" {
slot.ResourceKind = "asset"
}
if slot.Key == "" || slot.Name == "" || seen[slot.Key] {
return config, fmt.Errorf("输入素材槽位名称或标识无效")
}
seen[slot.Key] = true
if len(slot.MediaTypes) == 0 {
return config, fmt.Errorf("输入素材槽位必须指定媒体类型")
}
for mediaIndex, mediaType := range slot.MediaTypes {
mediaType = strings.TrimSpace(mediaType)
if mediaType != "image" && mediaType != "video" && mediaType != "audio" {
return config, fmt.Errorf("输入素材槽位媒体类型无效")
}
slot.MediaTypes[mediaIndex] = mediaType
}
if slot.MaxCount < 1 || slot.MaxCount > 20 || slot.MinCount < 0 || slot.MinCount > slot.MaxCount {
return config, fmt.Errorf("输入素材槽位数量范围无效")
}
if slot.Required && slot.MinCount < 1 {
slot.MinCount = 1
}
slot.CoverResourceID = strings.TrimSpace(slot.CoverResourceID)
slot.SampleResourceID = strings.TrimSpace(slot.SampleResourceID)
}
config.SearchKeywords = cleanStrings(config.SearchKeywords, 20)
config.Surfaces = cleanStrings(config.Surfaces, 8)
config.InputHint = strings.TrimSpace(config.InputHint)
config.ResultExample.Title = strings.TrimSpace(config.ResultExample.Title)
config.ResultExample.Description = strings.TrimSpace(config.ResultExample.Description)
config.ResultExample.ImageResourceIDs = cleanStrings(config.ResultExample.ImageResourceIDs, 8)
config.ResultExample.VideoResourceID = strings.TrimSpace(config.ResultExample.VideoResourceID)
if config.RecommendationOrder < 0 || config.RecommendationOrder > 1_000_000 {
return config, fmt.Errorf("推荐排序值无效")
}
return config, nil
}
func validCategoryID(categoryID string) bool {
switch categoryID {
case "recommended", "people", "ecommerce", "product", "drama-character", "drama-scene", "storyboard", "short-video", "scene", "enhancement":
return true
default:
return false
}
}
func cleanStrings(values []string, limit int) []string {
seen := map[string]bool{}
result := make([]string, 0, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" || seen[value] || len(result) >= limit {
continue
}
seen[value] = true
result = append(result, value)
}
return result
}
func catalogResourceIDs(config catalogConfig) []struct{ id, kind string } {
result := make([]struct{ id, kind string }, 0)
seen := map[string]bool{}
appendResource := func(id, kind string) {
id = strings.TrimSpace(id)
if id == "" || seen[id] {
return
}
seen[id] = true
result = append(result, struct{ id, kind string }{id: id, kind: kind})
}
for _, slot := range config.InputContract.Slots {
appendResource(slot.CoverResourceID, "image")
appendResource(slot.SampleResourceID, "image")
}
for _, id := range config.ResultExample.ImageResourceIDs {
appendResource(id, "image")
}
appendResource(config.ResultExample.VideoResourceID, "video")
return result
}