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

201 lines
7.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
})
}