Files
ttstd-infinite-canvas/backend/internal/repository/tools.go
T
ddCatandbensharp e93bf3ca39 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

201 lines
7.0 KiB
Go

package repository
import (
"errors"
"strings"
"time"
"infinite-canvas/backend/internal/kernel"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/tools"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// UpsertBuiltinTools 按 id 幂等更新内置工具;created_at 保留首次写入值。
func (r *Repository) UpsertBuiltinTools(tools []model.Tool) error {
if len(tools) == 0 {
return nil
}
return r.db.Transaction(func(tx *gorm.DB) error {
ids := make([]int64, 0, len(tools))
for _, tool := range tools {
ids = append(ids, tool.ID)
}
var collisions int64
if err := tx.Model(&model.Tool{}).Where("id IN ? AND source <> ?", ids, "builtin").Count(&collisions).Error; err != nil {
return err
}
if collisions > 0 {
return errors.New("内置工具 ID 与用户工具冲突,拒绝覆盖")
}
result := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
Where: clause.Where{Exprs: []clause.Expression{clause.Eq{Column: clause.Column{Table: "tools", Name: "source"}, Value: "builtin"}}},
DoUpdates: clause.AssignmentColumns([]string{
"type", "label_en", "label", "desc", "tag", "cover", "extra_info_json", "prompt", "ratio",
"media_url", "owner_id", "source", "enabled", "visibility", "sort_weight", "updated_at",
}),
}).Create(&tools)
if result.Error != nil {
return result.Error
}
if result.RowsAffected != int64(len(tools)) {
return errors.New("内置工具写入遇到 ID 冲突,已回滚")
}
// 内置工具显式写入自增主键,PostgreSQL 的序列不会自动前移;这里同步到当前最大值,
// 避免用户自建工具从 1 重新分配而与内置工具主键冲突。
if r.Dialect() != "postgres" {
return nil
}
return tx.Exec("SELECT setval(pg_get_serial_sequence('tools', 'id'), (SELECT COALESCE(MAX(id), 1) FROM tools), true)").Error
})
}
// ListTools 按范围查询工具列表并携带当前用户收藏状态。
func (r *Repository) ListTools(userID string, req tools.ToolListRequest) ([]tools.ToolWithFavorite, int64, error) {
base := r.db.Table("tools").Where("enabled = ? AND (tools.visibility = ? OR tools.owner_id = ?)", true, tools.ToolVisibilityPublic, userID)
switch req.Scope {
case tools.ToolScopeFavorites:
base = base.Joins("JOIN tool_favorites tf ON tf.tool_id = tools.id AND tf.user_id = ?", userID)
default: // public / custom
base = base.Joins("LEFT JOIN tool_favorites tf ON tf.tool_id = tools.id AND tf.user_id = ?", userID)
}
// 可见性:公共 scope 只看 public(含他人公开),其余 scope 只看自己可见的
switch req.Scope {
case tools.ToolScopePublic:
base = base.Where("tools.visibility = ?", tools.ToolVisibilityPublic)
case tools.ToolScopeFavorites:
base = base.Where("tf.user_id = ?", userID)
case tools.ToolScopeCustom:
base = base.Where("tools.owner_id = ? AND tools.source = ?", userID, tools.ToolSourceUser)
}
if req.Type != "" {
base = base.Where("tools.type = ?", req.Type)
}
if req.Tag != "" {
base = base.Where("tools.tag = ?", req.Tag)
}
if req.Search != "" {
keyword := "%" + strings.ToLower(req.Search) + "%"
base = base.Where("(LOWER(tools.label) LIKE ? OR LOWER(tools.label_en) LIKE ? OR LOWER(tools.desc) LIKE ?)", keyword, keyword, keyword)
}
var total int64
if err := base.Session(&gorm.Session{}).Count(&total).Error; err != nil {
return nil, 0, err
}
orderBy := "tools.sort_weight ASC, tools.id ASC"
switch req.Scope {
case tools.ToolScopeFavorites:
orderBy = "tf.created_at DESC, tools.id DESC"
}
offset := (req.Page - 1) * req.PageSize
var rows []struct {
model.Tool
TFID *int64 `gorm:"column:tf_id"`
TFCreatedAt *time.Time `gorm:"column:tf_created_at"`
}
// 列表只取摘要字段,不取 extra_info_json、prompt 两个大字段;列与 tools.ToolSummary 对齐。
selectColumns := "tools.id, tools.type, tools.label_en, tools.label, tools.desc, tools.tag, tools.cover, " +
"tools.ratio, tools.media_url, tools.owner_id, tools.source, tools.enabled, tools.visibility, " +
"tools.sort_weight, tools.created_at, tools.updated_at, tf.id AS tf_id, tf.created_at AS tf_created_at"
err := base.Session(&gorm.Session{}).
Select(selectColumns).
Order(orderBy).
Offset(offset).
Limit(req.PageSize).
Scan(&rows).Error
if err != nil {
return nil, 0, err
}
items := make([]tools.ToolWithFavorite, 0, len(rows))
for _, row := range rows {
items = append(items, tools.ToolWithFavorite{
Tool: row.Tool,
Favorited: row.TFID != nil,
FavoritedAt: row.TFCreatedAt,
})
}
return items, total, nil
}
// ToolForUser 校验可见性后返回工具。
func (r *Repository) ToolForUser(userID string, toolID int64) (model.Tool, error) {
var tool model.Tool
err := r.db.Where("id = ? AND enabled = ? AND (visibility = ? OR owner_id = ?)", toolID, true, tools.ToolVisibilityPublic, userID).
First(&tool).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return tool, kernel.NotFound("工具不存在或不可见")
}
if err != nil {
return tool, err
}
return tool, nil
}
// ToolFavorited 返回工具及收藏时间(未收藏时为 nil)。
func (r *Repository) ToolFavorited(userID string, toolID int64) (model.Tool, *time.Time, error) {
tool, err := r.ToolForUser(userID, toolID)
if err != nil {
return tool, nil, err
}
var favorite model.ToolFavorite
err = r.db.Where("user_id = ? AND tool_id = ?", userID, toolID).First(&favorite).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return tool, nil, nil
}
if err != nil {
return tool, nil, err
}
return tool, &favorite.CreatedAt, nil
}
// AddToolFavorite 幂等添加收藏;已收藏时直接成功。
func (r *Repository) AddToolFavorite(userID string, toolID int64) error {
favorite := model.ToolFavorite{UserID: userID, ToolID: toolID, CreatedAt: time.Now()}
return r.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}, {Name: "tool_id"}},
DoNothing: true,
}).Create(&favorite).Error
}
// RemoveToolFavorite 取消收藏;未收藏时也无错误。
func (r *Repository) RemoveToolFavorite(userID string, toolID int64) error {
return r.db.Where("user_id = ? AND tool_id = ?", userID, toolID).
Delete(&model.ToolFavorite{}).Error
}
// CreateTool 创建用户自定义工具。
func (r *Repository) CreateTool(tool *model.Tool) (*model.Tool, error) {
if err := r.db.Create(tool).Error; err != nil {
return nil, err
}
return tool, nil
}
// DeleteUserTool 仅允许删除自己的自定义工具;事务内同步清理收藏记录。
func (r *Repository) DeleteUserTool(userID string, toolID int64) error {
return r.db.Transaction(func(tx *gorm.DB) error {
var tool model.Tool
err := tx.Where("id = ? AND owner_id = ? AND source = ?", toolID, userID, tools.ToolSourceUser).
First(&tool).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return kernel.NotFound("工具不存在或不支持删除")
}
if err != nil {
return err
}
if err := tx.Where("tool_id = ?", toolID).Delete(&model.ToolFavorite{}).Error; err != nil {
return err
}
return tx.Delete(&model.Tool{}, toolID).Error
})
}