Files
ttstd-infinite-canvas/backend/internal/tools/tools_seed.go
ddCat 9148ceab66 feat(canvas): 画布工作区 - 整合侧栏与风格预设并修复提示词回填
选择性整合 #547,保留主分支节点与工具条设计。新增默认折叠的节点、资产、任务和历史面板,风格与预设保留在节点内,移除重复入口。

补齐工具权限、提示词解析、资源引用保护及数据库迁移,修复提示词清空和连续编辑后的生成配置同步。

验证:前端全量 1925 项测试、构建、类型检查及 lint 通过;后端 go test ./... 通过。真实模型、PostgreSQL 实库与登录态页面验收仍待进行。

Refs #547

Co-authored-by: bensharp <bensharp@qq.com>
2026-09-20 16:40:08 +08:00

164 lines
4.6 KiB
Go

package tools
import (
_ "embed"
"encoding/json"
"fmt"
"strings"
"time"
"infinite-canvas/backend/internal/model"
)
//go:embed seed/tools.json
var builtinToolsJSON []byte
type toolSeedItem struct {
ID int64 `json:"id"`
LabelEn string `json:"label_en"`
Label string `json:"label"`
Desc string `json:"desc"`
Tag string `json:"tag"`
Cover string `json:"cover"`
ExtraInfo []string `json:"extra_info"`
Prompt string `json:"prompt"`
Ratio string `json:"ratio"`
MediaURL string `json:"media_url"`
OwnerID string `json:"owner_id"`
Enabled bool `json:"enabled"`
Visibility string `json:"visibility"`
CreateAt string `json:"create_at"`
UpdateAt string `json:"update_at"`
}
type toolSeedGroup struct {
Title string `json:"title"`
Tags map[string]string `json:"tags"`
List []toolSeedItem `json:"list"`
}
type builtinToolsFile struct {
Style toolSeedGroup `json:"style"`
Motion toolSeedGroup `json:"motion"`
NineGrid toolSeedGroup `json:"nine_grid"`
}
const (
ToolTypeStyle = "style"
ToolTypeMotion = "motion"
ToolTypeNineGrid = "nine_grid"
ToolTypeEffect = "effect"
)
const seedTimeLayout = "2006-01-02 15:04:05"
// EnsureBuiltinTools 将预设工具列表幂等写入数据库;重复启动只更新内容字段,不删除已有数据。
// 初始化不下载第三方媒体,避免启动依赖外部 CDN 或产生无归属资源。
func EnsureBuiltinTools(repo Repository) error {
var file builtinToolsFile
if err := json.Unmarshal(builtinToolsJSON, &file); err != nil {
return fmt.Errorf("解析内置工具失败: %w", err)
}
var tools []model.Tool
sortWeight := 0
groups := []struct {
typ string
group toolSeedGroup
}{
{ToolTypeStyle, file.Style},
{ToolTypeMotion, file.Motion},
{ToolTypeNineGrid, file.NineGrid},
}
seen := make(map[int64]struct{})
for _, g := range groups {
for _, item := range g.group.List {
if item.ID <= 0 {
return fmt.Errorf("内置工具 ID 无效: type=%s label_en=%q", g.typ, item.LabelEn)
}
if _, exists := seen[item.ID]; exists {
return fmt.Errorf("内置工具 ID 重复: %d", item.ID)
}
seen[item.ID] = struct{}{}
uniqueKey := fmt.Sprintf("%s:%d", g.typ, item.ID)
labelEn := strings.TrimSpace(item.LabelEn)
if labelEn == "" {
return fmt.Errorf("内置工具英文标识为空: %s", uniqueKey)
}
label := strings.TrimSpace(item.Label)
if label == "" {
return fmt.Errorf("内置工具名称为空: %s", uniqueKey)
}
prompt := strings.TrimSpace(item.Prompt)
if prompt == "" {
return fmt.Errorf("内置工具提示词为空: %s", uniqueKey)
}
visibility := strings.TrimSpace(item.Visibility)
if visibility != "public" && visibility != "private" {
return fmt.Errorf("内置工具可见性无效: %s visibility=%q", uniqueKey, item.Visibility)
}
createdAt, err := parseSeedTime(item.CreateAt)
if err != nil {
return fmt.Errorf("内置工具创建时间无效: %s: %w", uniqueKey, err)
}
updatedAt, err := parseSeedTime(item.UpdateAt)
if err != nil {
return fmt.Errorf("内置工具更新时间无效: %s: %w", uniqueKey, err)
}
extraInfoJSON := ""
if len(item.ExtraInfo) > 0 {
data, err := json.Marshal(item.ExtraInfo)
if err != nil {
return fmt.Errorf("序列化工具 %s 扩展信息失败: %w", uniqueKey, err)
}
extraInfoJSON = string(data)
}
cover := strings.TrimSpace(item.Cover)
mediaURL := strings.TrimSpace(item.MediaURL)
sortWeight++
tools = append(tools, model.Tool{
ID: item.ID,
Type: g.typ,
LabelEn: labelEn,
Label: label,
Desc: strings.TrimSpace(item.Desc),
Tag: strings.TrimSpace(item.Tag),
Cover: cover,
ExtraInfoJSON: extraInfoJSON,
Prompt: prompt,
Ratio: strings.TrimSpace(item.Ratio),
MediaURL: mediaURL,
OwnerID: item.OwnerID,
Source: ToolSourceBuiltin,
Enabled: item.Enabled,
Visibility: visibility,
SortWeight: sortWeight,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
})
}
}
if len(tools) == 0 {
return fmt.Errorf("内置工具列表为空")
}
if err := repo.UpsertBuiltinTools(tools); err != nil {
return fmt.Errorf("同步内置工具失败: %w", err)
}
return nil
}
func parseSeedTime(value string) (time.Time, error) {
value = strings.TrimSpace(value)
if value == "" {
return time.Time{}, fmt.Errorf("时间为空")
}
return time.ParseInLocation(seedTimeLayout, value, time.Local)
}