Files
yuan-lab-llm--clawmanager/backend/internal/repository/skill_repository.go
T

349 lines
12 KiB
Go

package repository
import (
"fmt"
"strings"
"time"
"clawreef/internal/models"
"github.com/upper/db/v4"
)
type SkillRepository interface {
ListSkillsByUser(userID int) ([]models.Skill, error)
ListAllSkills() ([]models.Skill, error)
GetSkillByID(id int) (*models.Skill, error)
GetSkillByUserKey(userID int, skillKey string) (*models.Skill, error)
CreateSkill(skill *models.Skill) error
UpdateSkill(skill *models.Skill) error
DeleteSkill(id int) error
GetBlobByContentHash(hash string) (*models.SkillBlob, error)
GetBlobByID(id int) (*models.SkillBlob, error)
CreateBlob(blob *models.SkillBlob) error
UpdateBlob(blob *models.SkillBlob) error
ListVersionsBySkillID(skillID int) ([]models.SkillVersion, error)
GetVersionByID(id int) (*models.SkillVersion, error)
GetVersionBySkillAndBlob(skillID, blobID int) (*models.SkillVersion, error)
GetLatestVersionBySkillID(skillID int) (*models.SkillVersion, error)
CreateVersion(version *models.SkillVersion) error
ListInstanceSkills(instanceID int) ([]models.InstanceSkill, error)
GetInstanceSkill(instanceID, skillID int) (*models.InstanceSkill, error)
UpsertInstanceSkill(item *models.InstanceSkill) error
MarkMissingInstanceSkills(instanceID int, activeSkillIDs []int, observedAt time.Time) error
CreateScanResult(result *models.SkillScanResult) error
GetScanResultByID(id int) (*models.SkillScanResult, error)
ListScanResultsByBlobID(blobID int) ([]models.SkillScanResult, error)
GetLatestScanResultByBlobID(blobID int) (*models.SkillScanResult, error)
GetLatestScanResultBySkillID(skillID int) (*models.SkillScanResult, error)
}
type skillRepository struct{ sess db.Session }
func NewSkillRepository(sess db.Session) SkillRepository { return &skillRepository{sess: sess} }
func (r *skillRepository) ListSkillsByUser(userID int) ([]models.Skill, error) {
var items []models.Skill
if err := r.sess.Collection("skills").Find(db.Cond{"user_id": userID}).OrderBy("-updated_at", "-id").All(&items); err != nil {
return nil, fmt.Errorf("failed to list skills by user: %w", err)
}
return items, nil
}
func (r *skillRepository) ListAllSkills() ([]models.Skill, error) {
var items []models.Skill
if err := r.sess.Collection("skills").Find().OrderBy("-updated_at", "-id").All(&items); err != nil {
return nil, fmt.Errorf("failed to list all skills: %w", err)
}
return items, nil
}
func (r *skillRepository) GetSkillByID(id int) (*models.Skill, error) {
var item models.Skill
if err := r.sess.Collection("skills").Find(db.Cond{"id": id}).One(&item); err != nil {
if err == db.ErrNoMoreRows {
return nil, nil
}
return nil, fmt.Errorf("failed to get skill: %w", err)
}
return &item, nil
}
func (r *skillRepository) GetSkillByUserKey(userID int, skillKey string) (*models.Skill, error) {
var item models.Skill
if err := r.sess.Collection("skills").Find(db.Cond{"user_id": userID, "skill_key": skillKey}).One(&item); err != nil {
if err == db.ErrNoMoreRows {
return nil, nil
}
return nil, fmt.Errorf("failed to get skill by key: %w", err)
}
return &item, nil
}
func (r *skillRepository) CreateSkill(skill *models.Skill) error {
ensureTimestamps(&skill.CreatedAt, &skill.UpdatedAt)
res, err := r.sess.Collection("skills").Insert(skill)
if err != nil {
return fmt.Errorf("failed to create skill: %w", err)
}
if id, ok := res.ID().(int64); ok {
skill.ID = int(id)
}
return nil
}
func (r *skillRepository) UpdateSkill(skill *models.Skill) error {
if skill.UpdatedAt.IsZero() {
skill.UpdatedAt = time.Now().UTC()
}
if err := r.sess.Collection("skills").Find(db.Cond{"id": skill.ID}).Update(skill); err != nil {
return fmt.Errorf("failed to update skill: %w", err)
}
return nil
}
func (r *skillRepository) DeleteSkill(id int) error {
if err := r.sess.Collection("skills").Find(db.Cond{"id": id}).Delete(); err != nil {
return fmt.Errorf("failed to delete skill: %w", err)
}
return nil
}
func (r *skillRepository) GetBlobByContentHash(hash string) (*models.SkillBlob, error) {
var item models.SkillBlob
if err := r.sess.Collection("skill_blobs").Find(db.Cond{"content_hash": hash}).One(&item); err != nil {
if err == db.ErrNoMoreRows {
return nil, nil
}
return nil, fmt.Errorf("failed to get skill blob by content hash: %w", err)
}
return &item, nil
}
func (r *skillRepository) GetBlobByID(id int) (*models.SkillBlob, error) {
var item models.SkillBlob
if err := r.sess.Collection("skill_blobs").Find(db.Cond{"id": id}).One(&item); err != nil {
if err == db.ErrNoMoreRows {
return nil, nil
}
return nil, fmt.Errorf("failed to get skill blob: %w", err)
}
return &item, nil
}
func (r *skillRepository) CreateBlob(blob *models.SkillBlob) error {
ensureTimestamps(&blob.CreatedAt, &blob.UpdatedAt)
res, err := r.sess.Collection("skill_blobs").Insert(blob)
if err != nil {
return fmt.Errorf("failed to create skill blob: %w", err)
}
if id, ok := res.ID().(int64); ok {
blob.ID = int(id)
}
return nil
}
func (r *skillRepository) UpdateBlob(blob *models.SkillBlob) error {
if blob.UpdatedAt.IsZero() {
blob.UpdatedAt = time.Now().UTC()
}
if err := r.sess.Collection("skill_blobs").Find(db.Cond{"id": blob.ID}).Update(blob); err != nil {
return fmt.Errorf("failed to update skill blob: %w", err)
}
return nil
}
func (r *skillRepository) ListVersionsBySkillID(skillID int) ([]models.SkillVersion, error) {
var items []models.SkillVersion
if err := r.sess.Collection("skill_versions").Find(db.Cond{"skill_id": skillID}).OrderBy("-version_no", "-id").All(&items); err != nil {
return nil, fmt.Errorf("failed to list skill versions: %w", err)
}
return items, nil
}
func (r *skillRepository) GetVersionByID(id int) (*models.SkillVersion, error) {
var item models.SkillVersion
if err := r.sess.Collection("skill_versions").Find(db.Cond{"id": id}).One(&item); err != nil {
if err == db.ErrNoMoreRows {
return nil, nil
}
return nil, fmt.Errorf("failed to get skill version: %w", err)
}
return &item, nil
}
func (r *skillRepository) GetVersionBySkillAndBlob(skillID, blobID int) (*models.SkillVersion, error) {
var item models.SkillVersion
if err := r.sess.Collection("skill_versions").Find(db.Cond{"skill_id": skillID, "blob_id": blobID}).One(&item); err != nil {
if err == db.ErrNoMoreRows {
return nil, nil
}
return nil, fmt.Errorf("failed to get skill version by blob: %w", err)
}
return &item, nil
}
func (r *skillRepository) GetLatestVersionBySkillID(skillID int) (*models.SkillVersion, error) {
var item models.SkillVersion
if err := r.sess.Collection("skill_versions").Find(db.Cond{"skill_id": skillID}).OrderBy("-version_no", "-id").One(&item); err != nil {
if err == db.ErrNoMoreRows {
return nil, nil
}
return nil, fmt.Errorf("failed to get latest skill version: %w", err)
}
return &item, nil
}
func (r *skillRepository) CreateVersion(version *models.SkillVersion) error {
ensureTimestamps(&version.CreatedAt, &version.UpdatedAt)
res, err := r.sess.Collection("skill_versions").Insert(version)
if err != nil {
return fmt.Errorf("failed to create skill version: %w", err)
}
if id, ok := res.ID().(int64); ok {
version.ID = int(id)
}
return nil
}
func (r *skillRepository) ListInstanceSkills(instanceID int) ([]models.InstanceSkill, error) {
var items []models.InstanceSkill
if err := r.sess.Collection("instance_skills").Find(db.Cond{"instance_id": instanceID}).OrderBy("-updated_at", "-id").All(&items); err != nil {
return nil, fmt.Errorf("failed to list instance skills: %w", err)
}
return items, nil
}
func (r *skillRepository) GetInstanceSkill(instanceID, skillID int) (*models.InstanceSkill, error) {
var item models.InstanceSkill
if err := r.sess.Collection("instance_skills").Find(db.Cond{"instance_id": instanceID, "skill_id": skillID}).One(&item); err != nil {
if err == db.ErrNoMoreRows {
return nil, nil
}
return nil, fmt.Errorf("failed to get instance skill: %w", err)
}
return &item, nil
}
func (r *skillRepository) UpsertInstanceSkill(item *models.InstanceSkill) error {
existing, err := r.GetInstanceSkill(item.InstanceID, item.SkillID)
if err != nil {
return err
}
if existing == nil {
ensureTimestamps(&item.CreatedAt, &item.UpdatedAt)
res, err := r.sess.Collection("instance_skills").Insert(item)
if err != nil {
if !isDuplicateEntryError(err) {
return fmt.Errorf("failed to create instance skill: %w", err)
}
existing, err = r.GetInstanceSkill(item.InstanceID, item.SkillID)
if err != nil {
return err
}
if existing == nil {
return fmt.Errorf("failed to create instance skill: %w", err)
}
item.ID = existing.ID
item.CreatedAt = existing.CreatedAt
if item.UpdatedAt.IsZero() {
item.UpdatedAt = time.Now().UTC()
}
if err := r.sess.Collection("instance_skills").Find(db.Cond{"id": existing.ID}).Update(item); err != nil {
return fmt.Errorf("failed to update instance skill after duplicate insert: %w", err)
}
return nil
}
if id, ok := res.ID().(int64); ok {
item.ID = int(id)
}
return nil
}
item.ID = existing.ID
item.CreatedAt = existing.CreatedAt
if item.UpdatedAt.IsZero() {
item.UpdatedAt = time.Now().UTC()
}
if err := r.sess.Collection("instance_skills").Find(db.Cond{"id": existing.ID}).Update(item); err != nil {
return fmt.Errorf("failed to update instance skill: %w", err)
}
return nil
}
func isDuplicateEntryError(err error) bool {
if err == nil {
return false
}
return strings.Contains(strings.ToLower(err.Error()), "duplicate entry")
}
func (r *skillRepository) MarkMissingInstanceSkills(instanceID int, activeSkillIDs []int, observedAt time.Time) error {
find := r.sess.Collection("instance_skills").Find(db.Cond{"instance_id": instanceID})
if len(activeSkillIDs) > 0 {
find = find.And(db.Cond{"skill_id NOT IN": activeSkillIDs})
}
var items []models.InstanceSkill
if err := find.All(&items); err != nil && err != db.ErrNoMoreRows {
return fmt.Errorf("failed to list stale instance skills: %w", err)
}
for _, item := range items {
item.Status = "removed"
item.RemovedAt = &observedAt
item.UpdatedAt = observedAt
if err := r.sess.Collection("instance_skills").Find(db.Cond{"id": item.ID}).Update(item); err != nil {
return fmt.Errorf("failed to mark instance skill removed: %w", err)
}
}
return nil
}
func (r *skillRepository) CreateScanResult(result *models.SkillScanResult) error {
ensureTimestamps(&result.CreatedAt, &result.UpdatedAt)
res, err := r.sess.Collection("skill_scan_results").Insert(result)
if err != nil {
return fmt.Errorf("failed to create skill scan result: %w", err)
}
if id, ok := res.ID().(int64); ok {
result.ID = int(id)
}
return nil
}
func (r *skillRepository) GetScanResultByID(id int) (*models.SkillScanResult, error) {
var item models.SkillScanResult
if err := r.sess.Collection("skill_scan_results").Find(db.Cond{"id": id}).One(&item); err != nil {
if err == db.ErrNoMoreRows {
return nil, nil
}
return nil, fmt.Errorf("failed to get skill scan result: %w", err)
}
return &item, nil
}
func (r *skillRepository) ListScanResultsByBlobID(blobID int) ([]models.SkillScanResult, error) {
var items []models.SkillScanResult
if err := r.sess.Collection("skill_scan_results").Find(db.Cond{"blob_id": blobID}).OrderBy("-scanned_at", "-id").All(&items); err != nil {
return nil, fmt.Errorf("failed to list skill scan results: %w", err)
}
return items, nil
}
func (r *skillRepository) GetLatestScanResultByBlobID(blobID int) (*models.SkillScanResult, error) {
var item models.SkillScanResult
if err := r.sess.Collection("skill_scan_results").Find(db.Cond{"blob_id": blobID}).OrderBy("-scanned_at", "-id").One(&item); err != nil {
if err == db.ErrNoMoreRows {
return nil, nil
}
return nil, fmt.Errorf("failed to get latest skill scan result: %w", err)
}
return &item, nil
}
func (r *skillRepository) GetLatestScanResultBySkillID(skillID int) (*models.SkillScanResult, error) {
skill, err := r.GetSkillByID(skillID)
if err != nil || skill == nil || skill.LastScanResultID == nil {
return nil, err
}
return r.GetScanResultByID(*skill.LastScanResultID)
}