604 lines
22 KiB
Go
604 lines
22 KiB
Go
package workbench
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"yingce/backend/internal/kernel"
|
|
"yingce/backend/internal/model"
|
|
"yingce/backend/internal/repository"
|
|
)
|
|
|
|
const (
|
|
KindImage = model.WorkbenchKindImage
|
|
KindVideo = model.WorkbenchKindVideo
|
|
)
|
|
|
|
type AdminGate interface {
|
|
RequireAdmin(*model.User) error
|
|
AppendAudit(*model.User, string, string, string, string, any) error
|
|
}
|
|
|
|
type ResourceAccessIssuer interface {
|
|
WorkbenchResourceAccess(resourceID string) (string, error)
|
|
}
|
|
|
|
type Service struct {
|
|
repo *repository.Repository
|
|
admin AdminGate
|
|
access ResourceAccessIssuer
|
|
}
|
|
|
|
func New(repo *repository.Repository, admin AdminGate, access ResourceAccessIssuer) *Service {
|
|
return &Service{repo: repo, admin: admin, access: access}
|
|
}
|
|
|
|
type ToolInput struct {
|
|
Kind string `json:"kind"`
|
|
CategoryID string `json:"categoryId"`
|
|
Name string `json:"name"`
|
|
Subtitle string `json:"subtitle"`
|
|
Description string `json:"description"`
|
|
IconResourceID string `json:"iconResourceId"`
|
|
CoverResourceID string `json:"coverResourceId"`
|
|
SortOrder int `json:"sortOrder"`
|
|
Capability json.RawMessage `json:"capability"`
|
|
Guide json.RawMessage `json:"guide"`
|
|
Enabled bool `json:"enabled"`
|
|
Presets *[]ToolPresetInput `json:"presets,omitempty"`
|
|
}
|
|
|
|
// ToolPresetInput allows the editor to save a tool and its presets atomically.
|
|
type ToolPresetInput struct {
|
|
ID string `json:"id,omitempty"`
|
|
PresetInput
|
|
}
|
|
|
|
type PresetInput struct {
|
|
ToolID string `json:"toolId"`
|
|
Name string `json:"name"`
|
|
Summary string `json:"summary"`
|
|
PromptTemplate string `json:"promptTemplate"`
|
|
DefaultParameters json.RawMessage `json:"defaultParameters"`
|
|
SortOrder int `json:"sortOrder"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
type ToolView struct {
|
|
ID string `json:"id"`
|
|
Kind string `json:"kind"`
|
|
CategoryID string `json:"categoryId"`
|
|
Name string `json:"name"`
|
|
Subtitle string `json:"subtitle"`
|
|
Description string `json:"description"`
|
|
IconResourceID string `json:"iconResourceId,omitempty"`
|
|
CoverResourceID string `json:"coverResourceId,omitempty"`
|
|
IconURL string `json:"iconUrl,omitempty"`
|
|
CoverURL string `json:"coverUrl,omitempty"`
|
|
SortOrder int `json:"sortOrder"`
|
|
Capability json.RawMessage `json:"capability,omitempty"`
|
|
Guide json.RawMessage `json:"guide,omitempty"`
|
|
InputHint string `json:"inputHint,omitempty"`
|
|
SearchKeywords []string `json:"searchKeywords,omitempty"`
|
|
Surfaces []string `json:"surfaces,omitempty"`
|
|
Recommended bool `json:"recommended"`
|
|
RecommendationOrder int `json:"recommendationOrder"`
|
|
InputContract InputContractView `json:"inputContract"`
|
|
ResultExample ResultExampleView `json:"resultExample"`
|
|
Enabled bool `json:"enabled"`
|
|
Version int `json:"version"`
|
|
Presets []PresetView `json:"presets"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
type InputContractView struct {
|
|
PromptRequired bool `json:"promptRequired"`
|
|
MinTextReferences int `json:"minTextReferences"`
|
|
Slots []ReferenceSlotView `json:"slots"`
|
|
}
|
|
|
|
type ReferenceSlotView 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"`
|
|
CoverURL string `json:"coverUrl,omitempty"`
|
|
SampleResourceID string `json:"sampleResourceId,omitempty"`
|
|
SampleURL string `json:"sampleUrl,omitempty"`
|
|
}
|
|
|
|
type ResultExampleView struct {
|
|
Title string `json:"title,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
ImageResourceIDs []string `json:"imageResourceIds,omitempty"`
|
|
ImageURLs []string `json:"imageUrls,omitempty"`
|
|
VideoResourceID string `json:"videoResourceId,omitempty"`
|
|
VideoURL string `json:"videoUrl,omitempty"`
|
|
}
|
|
|
|
type PresetView struct {
|
|
ID string `json:"id"`
|
|
ToolID string `json:"toolId"`
|
|
Name string `json:"name"`
|
|
Summary string `json:"summary"`
|
|
PromptTemplate string `json:"promptTemplate"`
|
|
DefaultParameters json.RawMessage `json:"defaultParameters,omitempty"`
|
|
SortOrder int `json:"sortOrder"`
|
|
Enabled bool `json:"enabled"`
|
|
Version int `json:"version"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
func (s *Service) PublicTools(kind string) ([]ToolView, error) {
|
|
if !validKind(kind) {
|
|
return nil, kernel.BadAuthRequest("工作台类型无效")
|
|
}
|
|
tools, err := s.repo.WorkbenchTools(kind, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
presetsByTool, err := s.repo.WorkbenchEnabledPresetsForTools(toolIDs(tools))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
views := make([]ToolView, 0, len(tools))
|
|
for _, item := range tools {
|
|
view := toolView(item, presetsByTool[item.ID])
|
|
if s.access != nil {
|
|
if err := s.attachToolMedia(&view); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
views = append(views, view)
|
|
}
|
|
return views, nil
|
|
}
|
|
|
|
func (s *Service) AdminTools(actor *model.User, kind string) ([]ToolView, error) {
|
|
if err := s.requireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
if kind != "" && !validKind(kind) {
|
|
return nil, kernel.BadAuthRequest("工作台类型无效")
|
|
}
|
|
tools, err := s.repo.WorkbenchTools(kind, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
views := make([]ToolView, 0, len(tools))
|
|
for _, item := range tools {
|
|
presets, err := s.repo.WorkbenchPresets(item.ID, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
view := toolView(item, presets)
|
|
view.IconURL, view.CoverURL = "", ""
|
|
views = append(views, view)
|
|
}
|
|
return views, nil
|
|
}
|
|
|
|
func (s *Service) CreateTool(actor *model.User, req ToolInput) (*ToolView, error) {
|
|
if err := s.requireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
item, err := normalizeTool(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.validateToolResources(actor.ID, item.IconResourceID, item.CoverResourceID, item.CapabilityJSON, item.GuideJSON); err != nil {
|
|
return nil, err
|
|
}
|
|
item.ID, item.CreatedBy, item.UpdatedBy = kernel.NewID(), actor.ID, actor.ID
|
|
item.Version, item.CreatedAt, item.UpdatedAt = 1, time.Now(), time.Now()
|
|
presets, err := s.normalizeToolPresets(actor.ID, item.ID, req.Presets)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.repo.SaveWorkbenchToolWithPresets(item, presets, true); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.audit(actor, "workbench_tool.create", item.ID, "创建工作台工具", map[string]any{"kind": item.Kind, "enabled": item.Enabled}); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.adminToolView(item)
|
|
}
|
|
|
|
func (s *Service) UpdateTool(actor *model.User, id string, req ToolInput) (*ToolView, error) {
|
|
if err := s.requireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
before, err := s.repo.WorkbenchTool(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item, err := normalizeTool(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if item.Kind != before.Kind {
|
|
return nil, kernel.BadAuthRequest("工具类型创建后不可更改")
|
|
}
|
|
if err := s.validateToolResources(actor.ID, item.IconResourceID, item.CoverResourceID, item.CapabilityJSON, item.GuideJSON); err != nil {
|
|
return nil, err
|
|
}
|
|
item.ID, item.CreatedBy, item.CreatedAt = before.ID, before.CreatedBy, before.CreatedAt
|
|
item.UpdatedBy, item.UpdatedAt, item.Version = actor.ID, time.Now(), before.Version+1
|
|
presets, err := s.normalizeToolPresets(actor.ID, item.ID, req.Presets)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.repo.SaveWorkbenchToolWithPresets(item, presets, false); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.audit(actor, "workbench_tool.update", id, "更新工作台工具", map[string]any{"beforeVersion": before.Version, "version": item.Version, "enabled": item.Enabled}); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.adminToolView(item)
|
|
}
|
|
|
|
func (s *Service) SetToolEnabled(actor *model.User, id string, enabled bool) error {
|
|
if err := s.requireAdmin(actor); err != nil {
|
|
return err
|
|
}
|
|
item, err := s.repo.WorkbenchTool(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if enabled {
|
|
if strings.TrimSpace(item.Name) == "" || !validKind(item.Kind) {
|
|
return kernel.BadAuthRequest("工具缺少有效名称或类型")
|
|
}
|
|
if err := s.validateToolResources(actor.ID, item.IconResourceID, item.CoverResourceID, item.CapabilityJSON, item.GuideJSON); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
before := item.Enabled
|
|
item.Enabled, item.UpdatedBy, item.UpdatedAt, item.Version = enabled, actor.ID, time.Now(), item.Version+1
|
|
if err := s.repo.SaveWorkbenchTool(item); err != nil {
|
|
return err
|
|
}
|
|
return s.audit(actor, "workbench_tool.publish", id, map[bool]string{true: "发布工作台工具", false: "下线工作台工具"}[enabled], map[string]any{"before": before, "enabled": enabled, "version": item.Version})
|
|
}
|
|
|
|
func (s *Service) CreatePreset(actor *model.User, req PresetInput) (*PresetView, error) {
|
|
if err := s.requireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
tool, err := s.repo.WorkbenchTool(strings.TrimSpace(req.ToolID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item, err := normalizePreset(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item.ID, item.CreatedBy, item.UpdatedBy = kernel.NewID(), actor.ID, actor.ID
|
|
item.Version, item.CreatedAt, item.UpdatedAt = 1, time.Now(), time.Now()
|
|
if err := s.repo.CreateWorkbenchPreset(item); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.audit(actor, "workbench_preset.create", item.ID, "创建工作台提示词预设", map[string]any{"toolId": tool.ID, "enabled": item.Enabled}); err != nil {
|
|
return nil, err
|
|
}
|
|
view := presetView(*item)
|
|
return &view, nil
|
|
}
|
|
|
|
func (s *Service) UpdatePreset(actor *model.User, id string, req PresetInput) (*PresetView, error) {
|
|
if err := s.requireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
before, err := s.repo.WorkbenchPreset(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item, err := normalizePreset(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if item.ToolID != before.ToolID {
|
|
return nil, kernel.BadAuthRequest("预设所属工具不可更改")
|
|
}
|
|
item.ID, item.CreatedBy, item.CreatedAt = before.ID, before.CreatedBy, before.CreatedAt
|
|
item.Version, item.UpdatedBy, item.UpdatedAt = before.Version+1, actor.ID, time.Now()
|
|
if err := s.repo.SaveWorkbenchPreset(item); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.audit(actor, "workbench_preset.update", id, "更新工作台提示词预设", map[string]any{"beforeVersion": before.Version, "version": item.Version, "enabled": item.Enabled}); err != nil {
|
|
return nil, err
|
|
}
|
|
view := presetView(*item)
|
|
return &view, nil
|
|
}
|
|
|
|
func (s *Service) SetPresetEnabled(actor *model.User, id string, enabled bool) error {
|
|
if err := s.requireAdmin(actor); err != nil {
|
|
return err
|
|
}
|
|
item, err := s.repo.WorkbenchPreset(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := s.repo.WorkbenchTool(item.ToolID); err != nil {
|
|
return err
|
|
}
|
|
before := item.Enabled
|
|
item.Enabled, item.Version, item.UpdatedBy, item.UpdatedAt = enabled, item.Version+1, actor.ID, time.Now()
|
|
if err := s.repo.SaveWorkbenchPreset(item); err != nil {
|
|
return err
|
|
}
|
|
return s.audit(actor, "workbench_preset.publish", id, map[bool]string{true: "启用工作台预设", false: "停用工作台预设"}[enabled], map[string]any{"before": before, "enabled": enabled, "version": item.Version})
|
|
}
|
|
|
|
func (s *Service) adminToolView(item *model.WorkbenchTool) (*ToolView, error) {
|
|
presets, err := s.repo.WorkbenchPresets(item.ID, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
view := toolView(*item, presets)
|
|
view.IconURL, view.CoverURL = "", ""
|
|
return &view, nil
|
|
}
|
|
|
|
func (s *Service) requireAdmin(actor *model.User) error {
|
|
if s == nil || s.admin == nil {
|
|
return errors.New("工作台管理权限未配置")
|
|
}
|
|
return s.admin.RequireAdmin(actor)
|
|
}
|
|
func (s *Service) audit(actor *model.User, action, id, summary string, data any) error {
|
|
if s == nil || s.admin == nil {
|
|
return errors.New("工作台审计未配置")
|
|
}
|
|
return s.admin.AppendAudit(actor, action, "workbench", id, summary, data)
|
|
}
|
|
|
|
func (s *Service) normalizeToolPresets(actorID, toolID string, inputs *[]ToolPresetInput) (*[]model.WorkbenchPreset, error) {
|
|
if inputs == nil {
|
|
return nil, nil
|
|
}
|
|
if len(*inputs) > 50 {
|
|
return nil, kernel.BadAuthRequest("每个工具最多配置 50 条预设")
|
|
}
|
|
items := make([]model.WorkbenchPreset, 0, len(*inputs))
|
|
seen := map[string]bool{}
|
|
for _, input := range *inputs {
|
|
input.ToolID = toolID
|
|
item, err := normalizePreset(input.PresetInput)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item.ID = strings.TrimSpace(input.ID)
|
|
if item.ID == "" {
|
|
item.ID = kernel.NewID()
|
|
} else {
|
|
before, err := s.repo.WorkbenchPreset(item.ID)
|
|
if err != nil || before.ToolID != toolID {
|
|
return nil, kernel.BadAuthRequest("预设不属于当前工具")
|
|
}
|
|
item.CreatedBy, item.CreatedAt, item.Version = before.CreatedBy, before.CreatedAt, before.Version
|
|
}
|
|
if seen[item.ID] {
|
|
return nil, kernel.BadAuthRequest("预设重复")
|
|
}
|
|
seen[item.ID] = true
|
|
if item.CreatedBy == "" {
|
|
item.CreatedBy, item.CreatedAt = actorID, time.Now()
|
|
}
|
|
item.UpdatedBy, item.UpdatedAt, item.Version = actorID, time.Now(), item.Version+1
|
|
items = append(items, *item)
|
|
}
|
|
return &items, nil
|
|
}
|
|
|
|
func (s *Service) validateToolResources(adminID, iconID, coverID, capabilityJSON, guideJSON string) error {
|
|
seen := map[string]bool{}
|
|
for _, id := range []string{iconID, coverID} {
|
|
id = strings.TrimSpace(id)
|
|
if id == "" || seen[id] {
|
|
continue
|
|
}
|
|
seen[id] = true
|
|
resource, err := s.repo.ResourceForUser(adminID, id)
|
|
if err != nil || resource.Status != model.ResourceStatusReady || resource.Kind != "image" {
|
|
return kernel.BadAuthRequest("工具图标和封面必须引用当前管理员已就绪的图片资源")
|
|
}
|
|
}
|
|
config, err := parseCatalogConfig(capabilityJSON, guideJSON)
|
|
if err != nil {
|
|
return kernel.BadAuthRequest("工具输入配置无效")
|
|
}
|
|
for _, reference := range catalogResourceIDs(config) {
|
|
if seen[reference.id] {
|
|
continue
|
|
}
|
|
seen[reference.id] = true
|
|
resource, resourceErr := s.repo.ResourceForUser(adminID, reference.id)
|
|
if resourceErr != nil || resource.Status != model.ResourceStatusReady || resource.Kind != reference.kind {
|
|
return kernel.BadAuthRequest("工具输入槽位和效果示例必须引用当前管理员已就绪的对应媒体")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) attachToolMedia(view *ToolView) error {
|
|
if s.access == nil {
|
|
return nil
|
|
}
|
|
if view.IconResourceID != "" {
|
|
url, err := s.access.WorkbenchResourceAccess(view.IconResourceID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
view.IconURL = url
|
|
}
|
|
if view.CoverResourceID != "" {
|
|
url, err := s.access.WorkbenchResourceAccess(view.CoverResourceID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
view.CoverURL = url
|
|
}
|
|
for index := range view.InputContract.Slots {
|
|
slot := &view.InputContract.Slots[index]
|
|
if slot.CoverResourceID != "" {
|
|
if url, err := s.access.WorkbenchResourceAccess(slot.CoverResourceID); err == nil {
|
|
slot.CoverURL = url
|
|
}
|
|
}
|
|
if slot.SampleResourceID != "" {
|
|
if url, err := s.access.WorkbenchResourceAccess(slot.SampleResourceID); err == nil {
|
|
slot.SampleURL = url
|
|
}
|
|
}
|
|
}
|
|
for _, resourceID := range view.ResultExample.ImageResourceIDs {
|
|
if url, err := s.access.WorkbenchResourceAccess(resourceID); err == nil {
|
|
view.ResultExample.ImageURLs = append(view.ResultExample.ImageURLs, url)
|
|
}
|
|
}
|
|
if view.ResultExample.VideoResourceID != "" {
|
|
if url, err := s.access.WorkbenchResourceAccess(view.ResultExample.VideoResourceID); err == nil {
|
|
view.ResultExample.VideoURL = url
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeTool(req ToolInput) (*model.WorkbenchTool, error) {
|
|
item := &model.WorkbenchTool{
|
|
Kind: strings.TrimSpace(req.Kind), CategoryID: strings.TrimSpace(req.CategoryID), Name: strings.TrimSpace(req.Name),
|
|
Subtitle: strings.TrimSpace(req.Subtitle), Description: strings.TrimSpace(req.Description), IconResourceID: strings.TrimSpace(req.IconResourceID),
|
|
CoverResourceID: strings.TrimSpace(req.CoverResourceID), SortOrder: req.SortOrder, Enabled: req.Enabled,
|
|
}
|
|
if !validKind(item.Kind) {
|
|
return nil, kernel.BadAuthRequest("工作台类型只能是 image 或 video")
|
|
}
|
|
if !validCategoryID(item.CategoryID) {
|
|
return nil, kernel.BadAuthRequest("工具分类无效")
|
|
}
|
|
if item.Name == "" || utf8.RuneCountInString(item.Name) > 120 || utf8.RuneCountInString(item.CategoryID) > 64 || utf8.RuneCountInString(item.Subtitle) > 240 || utf8.RuneCountInString(item.Description) > 12000 {
|
|
return nil, kernel.BadAuthRequest("工具名称、分类或说明长度无效")
|
|
}
|
|
if item.SortOrder < 0 || item.SortOrder > 1_000_000 {
|
|
return nil, kernel.BadAuthRequest("工具排序值无效")
|
|
}
|
|
var err error
|
|
item.CapabilityJSON, err = optionalJSONObject(req.Capability)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("capability: %w", err)
|
|
}
|
|
item.GuideJSON, err = optionalJSONObject(req.Guide)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("guide: %w", err)
|
|
}
|
|
config, err := parseCatalogConfig(item.CapabilityJSON, item.GuideJSON)
|
|
if err != nil {
|
|
return nil, kernel.BadAuthRequest("工具预设结构无效")
|
|
}
|
|
config, err = normalizeCatalogConfig(config)
|
|
if err != nil {
|
|
return nil, kernel.BadAuthRequest(err.Error())
|
|
}
|
|
capability := map[string]any{}
|
|
guide := map[string]any{}
|
|
_ = json.Unmarshal([]byte(item.CapabilityJSON), &capability)
|
|
_ = json.Unmarshal([]byte(item.GuideJSON), &guide)
|
|
capability["inputContract"] = config.InputContract
|
|
capability["searchKeywords"], capability["surfaces"] = config.SearchKeywords, config.Surfaces
|
|
capability["recommended"], capability["recommendationOrder"] = config.Recommended, config.RecommendationOrder
|
|
guide["inputHint"], guide["resultExample"] = config.InputHint, config.ResultExample
|
|
encodedCapability, err := json.Marshal(capability)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
encodedGuide, err := json.Marshal(guide)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item.CapabilityJSON, item.GuideJSON = string(encodedCapability), string(encodedGuide)
|
|
return item, nil
|
|
}
|
|
|
|
func normalizePreset(req PresetInput) (*model.WorkbenchPreset, error) {
|
|
item := &model.WorkbenchPreset{
|
|
ToolID: strings.TrimSpace(req.ToolID), Name: strings.TrimSpace(req.Name), Summary: strings.TrimSpace(req.Summary),
|
|
PromptTemplate: strings.TrimSpace(req.PromptTemplate), SortOrder: req.SortOrder, Enabled: req.Enabled,
|
|
}
|
|
if item.ToolID == "" || item.Name == "" || item.PromptTemplate == "" || utf8.RuneCountInString(item.Name) > 120 || utf8.RuneCountInString(item.Summary) > 500 || utf8.RuneCountInString(item.PromptTemplate) > 12000 {
|
|
return nil, kernel.BadAuthRequest("预设工具、名称、提示词或文本长度无效")
|
|
}
|
|
if item.SortOrder < 0 || item.SortOrder > 1_000_000 {
|
|
return nil, kernel.BadAuthRequest("预设排序值无效")
|
|
}
|
|
var err error
|
|
item.DefaultParamsJSON, err = optionalJSONObject(req.DefaultParameters)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("defaultParameters: %w", err)
|
|
}
|
|
return item, nil
|
|
}
|
|
|
|
func optionalJSONObject(raw json.RawMessage) (string, error) {
|
|
if len(raw) == 0 || string(raw) == "null" {
|
|
return "{}", nil
|
|
}
|
|
var value map[string]any
|
|
if err := json.Unmarshal(raw, &value); err != nil || value == nil {
|
|
return "", kernel.BadAuthRequest("字段必须是 JSON 对象")
|
|
}
|
|
return string(raw), nil
|
|
}
|
|
|
|
func toolIDs(items []model.WorkbenchTool) []string {
|
|
ids := make([]string, 0, len(items))
|
|
for _, item := range items {
|
|
ids = append(ids, item.ID)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func validKind(kind string) bool { return kind == KindImage || kind == KindVideo }
|
|
|
|
func toolView(item model.WorkbenchTool, presets []model.WorkbenchPreset) ToolView {
|
|
config, _ := parseCatalogConfig(item.CapabilityJSON, item.GuideJSON)
|
|
view := ToolView{
|
|
ID: item.ID, Kind: item.Kind, CategoryID: item.CategoryID, Name: item.Name, Subtitle: item.Subtitle, Description: item.Description,
|
|
IconResourceID: item.IconResourceID, CoverResourceID: item.CoverResourceID, SortOrder: item.SortOrder,
|
|
Capability: json.RawMessage(item.CapabilityJSON), Guide: json.RawMessage(item.GuideJSON), InputHint: config.InputHint,
|
|
SearchKeywords: config.SearchKeywords, Surfaces: config.Surfaces, Recommended: config.Recommended, RecommendationOrder: config.RecommendationOrder,
|
|
InputContract: inputContractView(config.InputContract), ResultExample: resultExampleView(config.ResultExample), Enabled: item.Enabled, Version: item.Version, UpdatedAt: item.UpdatedAt,
|
|
Presets: make([]PresetView, 0, len(presets)),
|
|
}
|
|
for _, preset := range presets {
|
|
view.Presets = append(view.Presets, presetView(preset))
|
|
}
|
|
return view
|
|
}
|
|
|
|
func inputContractView(contract InputContract) InputContractView {
|
|
view := InputContractView{PromptRequired: contract.PromptRequired, MinTextReferences: contract.MinTextReferences, Slots: make([]ReferenceSlotView, 0, len(contract.Slots))}
|
|
for _, slot := range contract.Slots {
|
|
view.Slots = append(view.Slots, ReferenceSlotView{Key: slot.Key, Name: slot.Name, MediaTypes: slot.MediaTypes, ResourceKind: slot.ResourceKind, MaxCount: slot.MaxCount, Required: slot.Required, MinCount: slot.MinCount, CoverResourceID: slot.CoverResourceID, SampleResourceID: slot.SampleResourceID})
|
|
}
|
|
return view
|
|
}
|
|
|
|
func resultExampleView(example ResultExample) ResultExampleView {
|
|
return ResultExampleView{Title: example.Title, Description: example.Description, ImageResourceIDs: example.ImageResourceIDs, VideoResourceID: example.VideoResourceID}
|
|
}
|
|
|
|
func presetView(item model.WorkbenchPreset) PresetView {
|
|
return PresetView{ID: item.ID, ToolID: item.ToolID, Name: item.Name, Summary: item.Summary, PromptTemplate: item.PromptTemplate,
|
|
DefaultParameters: json.RawMessage(item.DefaultParamsJSON), SortOrder: item.SortOrder, Enabled: item.Enabled, Version: item.Version, UpdatedAt: item.UpdatedAt}
|
|
}
|