183 lines
6.6 KiB
Go
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
|
|
}
|