feat(skills): 技能管理 - 增加可选的平台子分类策展 (#693)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Catlaxy AIandClaude Opus 5.5 authored and GitHub committed 2026-10-06 09:24:21 +08:00
1 parent 5976389ed4
commit c963fc7b89
32 files changed
+981 -24

No files matched your search

@@ -21,6 +21,13 @@ func (r *Repository) BuiltinSkillTombstoned(skillID string) (bool, error) {
func (r *Repository) DeleteBuiltinSkill(skillID string, actorID string) error {
now := time.Now().UTC()
return r.db.Transaction(func(tx *gorm.DB) error {
var locked model.Skill
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&locked, "id = ?", skillID).Error; err != nil {
return err
}
if err := tx.Delete(&model.SkillCurationAssignment{}, "skill_id = ?", skillID).Error; err != nil {
return err
}
if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "skill_id"}}, DoUpdates: clause.AssignmentColumns([]string{"deleted_by", "deleted_at"})}).Create(&model.BuiltinSkillTombstone{SkillID: skillID, DeletedBy: actorID, DeletedAt: now}).Error; err != nil {
return err
}
@@ -0,0 +1,114 @@
package repository
import (
"encoding/json"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"infinite-canvas/backend/internal/kernel"
"infinite-canvas/backend/internal/model"
)
type SkillCuration struct {
model.SkillCurationSetting
Categories []model.SkillCurationCategory `json:"categories"`
Assignments []model.SkillCurationAssignment `json:"assignments"`
}
// Read the version before and after the snapshot so concurrent edits cannot
// produce a mixed snapshot that a client could save with a newer revision.
func (r *Repository) SkillCuration(admin bool) (*SkillCuration, error) {
for attempt := 0; attempt < 3; attempt++ {
out := &SkillCuration{Categories: []model.SkillCurationCategory{}, Assignments: []model.SkillCurationAssignment{}}
if err := r.db.First(&out.SkillCurationSetting, 1).Error; err != nil {
return nil, err
}
if !admin && !out.Enabled {
return out, nil
}
query := r.db.Order("sort_order, id")
if !admin {
query = query.Where("enabled = ?", true)
}
if err := query.Find(&out.Categories).Error; err != nil {
return nil, err
}
query = r.db.Model(&model.SkillCurationAssignment{}).Select("skill_curation_assignments.*").
Joins("JOIN skills ON skills.id = skill_curation_assignments.skill_id").Where("skills.status = 1 AND skills.is_private = false")
if !admin {
query = query.Joins("JOIN skill_curation_categories c ON c.id = skill_curation_assignments.category_id").Where("c.enabled = true AND c.root_tag = skills.tag")
}
if err := query.Order("skill_id, category_id").Find(&out.Assignments).Error; err != nil {
return nil, err
}
var after model.SkillCurationSetting
if err := r.db.First(&after, 1).Error; err != nil {
return nil, err
}
if after.Revision == out.Revision {
return out, nil
}
}
return nil, kernel.NewAppError(409, "分类正在更新,请重新加载")
}
func (r *Repository) UpdateSkillCuration(actorID string, revision int64, requestID string, change func(*Repository) error) error {
return r.db.Transaction(func(tx *gorm.DB) error {
result := tx.Model(&model.SkillCurationSetting{}).Where("id = 1 AND revision = ?", revision).Update("revision", revision+1)
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return kernel.NewAppError(409, "分类版本已更新,请重新加载")
}
var actor model.User
if err := tx.First(&actor, "id = ?", actorID).Error; err != nil {
return err
}
if actor.Role != model.UserRoleAdmin || actor.Status != model.UserStatusActive {
return kernel.Forbidden("需要管理员权限")
}
repo := New(tx)
before, err := repo.SkillCuration(true)
if err != nil {
return err
}
if err := change(repo); err != nil {
return err
}
after, err := repo.SkillCuration(true)
if err != nil {
return err
}
before.Revision = revision
metadata, err := json.Marshal(map[string]any{"requestId": requestID, "before": before, "after": after})
if err != nil {
return err
}
return tx.Create(&model.AdminAuditEvent{ID: kernel.NewID(), ActorUserID: actorID, Action: "skill_curation.update", TargetType: "skill_curation", TargetID: "1", MetadataJSON: string(metadata)}).Error
})
}
func (r *Repository) SetSkillCurationEnabled(enabled bool) error {
return r.db.Model(&model.SkillCurationSetting{}).Where("id = 1").Update("enabled", enabled).Error
}
func (r *Repository) SaveSkillCurationCategory(category *model.SkillCurationCategory) error {
return r.db.Save(category).Error
}
func (r *Repository) CurationSkill(id string) (*model.Skill, error) {
var row model.Skill
err := r.db.Clauses(clause.Locking{Strength: "UPDATE"}).First(&row, "id = ? AND status = 1 AND is_private = false", id).Error
return &row, err
}
func (r *Repository) AssignSkillCuration(skillID string, ids []string) error {
if err := r.db.Delete(&model.SkillCurationAssignment{}, "skill_id = ?", skillID).Error; err != nil {
return err
}
for _, id := range ids {
if err := r.db.Create(&model.SkillCurationAssignment{SkillID: skillID, CategoryID: id}).Error; err != nil {
return err
}
}
return nil
}
const effectiveCurationCategory = `SELECT 1 FROM skill_curation_assignments a JOIN skill_curation_categories c ON c.id = a.category_id WHERE a.skill_id = skills.id AND c.enabled = true AND c.root_tag = skills.tag AND skills.is_private = false`
+24 -9
View File
@@ -11,15 +11,17 @@ import (
)
type SkillListFilter struct {
UserID string
Scope string
Search string
Tag string
LibraryCategoryID string
LibraryUncategorized bool
Sort string
Limit int
Offset int
PlatformCategoryID string
PlatformUncategorized bool
UserID string
Scope string
Search string
Tag string
LibraryCategoryID string
LibraryUncategorized bool
Sort string
Limit int
Offset int
}
type SkillMetrics struct {
@@ -77,6 +79,12 @@ func (r *Repository) Skills(filter SkillListFilter) ([]model.Skill, int64, error
if filter.Tag != "" {
query = query.Where("skills.tag = ?", filter.Tag)
}
if filter.PlatformCategoryID != "" {
query = query.Where("EXISTS ("+effectiveCurationCategory+" AND c.id = ?)", filter.PlatformCategoryID)
}
if filter.PlatformUncategorized {
query = query.Where("NOT EXISTS (" + effectiveCurationCategory + ")")
}
if filter.LibraryCategoryID != "" {
query = query.Where("user_skill_states.library_category_id = ?", filter.LibraryCategoryID)
} else if filter.LibraryUncategorized {
@@ -132,6 +140,13 @@ func (r *Repository) CreateSkill(skill *model.Skill, ownerState *model.UserSkill
func (r *Repository) DeleteSkill(id string) error {
return r.db.Transaction(func(tx *gorm.DB) error {
var locked model.Skill
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&locked, "id = ?", id).Error; err != nil {
return err
}
if err := tx.Delete(&model.SkillCurationAssignment{}, "skill_id = ?", id).Error; err != nil {
return err
}
if err := tx.Delete(&model.UserSkillState{}, "skill_id = ?", id).Error; err != nil {
return err
}