选择性整合 #547,保留主分支节点与工具条设计。新增默认折叠的节点、资产、任务和历史面板,风格与预设保留在节点内,移除重复入口。 补齐工具权限、提示词解析、资源引用保护及数据库迁移,修复提示词清空和连续编辑后的生成配置同步。 验证:前端全量 1925 项测试、构建、类型检查及 lint 通过;后端 go test ./... 通过。真实模型、PostgreSQL 实库与登录态页面验收仍待进行。 Refs #547 Co-authored-by: bensharp <bensharp@qq.com>
201 lines
7.0 KiB
Go
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
|
||
})
|
||
}
|