diff --git a/eai_agentplatform/backend-go/internal/repository/skill_definition.go b/eai_agentplatform/backend-go/internal/repository/skill_definition.go new file mode 100644 index 0000000..8120c70 --- /dev/null +++ b/eai_agentplatform/backend-go/internal/repository/skill_definition.go @@ -0,0 +1,69 @@ +package repository + +import ( + skillmodel "eai_agentplatform/backend/internal/skills/model" +) + +// SkillDefinition 技能定义仓库。 +// +// 技能是对象层三类一级对象之一(专员 / 技能 / 应用),与专员目录一样, +// 「按 key 取技能」原先在多个文件里各写一遍,统一收这里。 +type SkillDefinitionRepo struct{ *QueryBuilder } + +// List 技能定义列表(sort_order ASC, id ASC)。 +// +// state 不在这里定默认值:「不传 state 就只看 active」是列表接口的契约, +// 由 handler 解析 query 参数后把结果传进来(与 ActionDefinitionRepo.List 同一约定)。 +// +// exposedToUser 是三态:nil 表示不按该列过滤,非 nil 按值精确匹配。 +// 不用 bool 是因为「不传该参数」与「传 false」语义不同,前者要全量。 +func (r SkillDefinitionRepo) List(state string, exposedToUser *bool) []skillmodel.SkillDefinition { + q := r.Type(&skillmodel.SkillDefinition{}) + if state != "" { + q = q.Where("state = ?", state) + } + if exposedToUser != nil { + q = q.Where("exposed_to_user = ?", *exposedToUser) + } + var items []skillmodel.SkillDefinition + if q.Order("sort_order ASC, id ASC").Find(&items) { + return items + } + return nil +} + +// GetByID 按 ID 取。 +func (r SkillDefinitionRepo) GetByID(id uint) (skillmodel.SkillDefinition, bool) { + var s skillmodel.SkillDefinition + if r.Type(&s).Where("id = ?", id).First(&s) { + return s, true + } + return skillmodel.SkillDefinition{}, false +} + +// GetByKey 按 key 取(key 上有唯一索引)。 +func (r SkillDefinitionRepo) GetByKey(key string) (skillmodel.SkillDefinition, bool) { + var s skillmodel.SkillDefinition + if r.Type(&s).Where("key = ?", key).First(&s) { + return s, true + } + return skillmodel.SkillDefinition{}, false +} + +// Insert 新建技能定义。 +func (r SkillDefinitionRepo) Insert(s *skillmodel.SkillDefinition) bool { + return r.QueryBuilder.Insert(s) +} + +// Update 更新技能定义。 +func (r SkillDefinitionRepo) Update(s *skillmodel.SkillDefinition) bool { + return r.Save(s) +} + +// Delete 硬删除技能定义。 +// +// 表上没有软删字段,删了就是删了 —— 跟「停用」(state=inactive)是两回事: +// 停用还留着记录,挂在它名下的技能引用仍能解释。 +func (r SkillDefinitionRepo) Delete(s *skillmodel.SkillDefinition) bool { + return r.QueryBuilder.Delete(s) +} diff --git a/eai_agentplatform/backend-go/internal/repository/xapp_center.go b/eai_agentplatform/backend-go/internal/repository/xapp_center.go new file mode 100644 index 0000000..4d239a3 --- /dev/null +++ b/eai_agentplatform/backend-go/internal/repository/xapp_center.go @@ -0,0 +1,39 @@ +package repository + +import ( + "errors" + + "gorm.io/gorm" + + xappmodel "eai_agentplatform/backend/internal/xapps/model" +) + +// UserXAppCenter 用户自己的应用中心状态仓库(收藏 / 最近使用 / 自定义应用,每人一份)。 +// +// 这里的方法签名**有意不跟**其它仓库的 bool 惯例:FindByUser 返回 error。 +// 原因是本仓库的「没找到」是一个有意义的第三态 —— 它表示「该用户还没配过, +// 调用方要新建一份」。若压成 bool,一旦读取真出错(SQLite 本地锁等待是常事) +// 就会被当成「没配过」,转而写一份空白配置,把用户已有的收藏和最近使用抹掉。 +// user_id 上有唯一索引,这种误写多半会撞唯一键而失败,但那是运气不是设计。 +type UserXAppCenterRepo struct{ *QueryBuilder } + +// FindByUser 取某用户的应用中心配置。三种结果: +// - row != nil 找到 +// - row == nil, err == nil 该用户还没有配置,调用方应新建(不是错误) +// - err != nil 读取出错,调用方应报错,**不要**当成「没有配置」 +func (r UserXAppCenterRepo) FindByUser(userID uint) (*xappmodel.UserXAppCenter, error) { + var row xappmodel.UserXAppCenter + err := r.Inner().Where("user_id = ?", userID).First(&row).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + if err != nil { + return nil, err + } + return &row, nil +} + +// Save 保存应用中心配置(新建或更新,由 row.ID 是否为零值决定)。 +func (r UserXAppCenterRepo) Save(row *xappmodel.UserXAppCenter) bool { + return r.QueryBuilder.Save(row) +} diff --git a/eai_agentplatform/backend-go/internal/repository/xapp_definition.go b/eai_agentplatform/backend-go/internal/repository/xapp_definition.go new file mode 100644 index 0000000..3f03690 --- /dev/null +++ b/eai_agentplatform/backend-go/internal/repository/xapp_definition.go @@ -0,0 +1,63 @@ +package repository + +import ( + xappmodel "eai_agentplatform/backend/internal/xapps/model" +) + +// XAppDefinition 应用定义仓库。 +// +// 应用(App)是对象层三类一级对象之一,定位是「长程任务运行壳」。 +// 本仓库只管应用目录本身的取数;用户自己的应用中心状态见 UserXAppCenterRepo。 +type XAppDefinitionRepo struct{ *QueryBuilder } + +// List 应用定义列表(sort_order ASC, id ASC)。 +// +// state 与 exposedToUser 的约定同 SkillDefinitionRepo.List: +// state 的默认值由 handler 定,exposedToUser 用三态指针区分「不传」与「传 false」。 +func (r XAppDefinitionRepo) List(state string, exposedToUser *bool) []xappmodel.XAppDefinition { + q := r.Type(&xappmodel.XAppDefinition{}) + if state != "" { + q = q.Where("state = ?", state) + } + if exposedToUser != nil { + q = q.Where("exposed_to_user = ?", *exposedToUser) + } + var items []xappmodel.XAppDefinition + if q.Order("sort_order ASC, id ASC").Find(&items) { + return items + } + return nil +} + +// GetByID 按 ID 取。 +func (r XAppDefinitionRepo) GetByID(id uint) (xappmodel.XAppDefinition, bool) { + var a xappmodel.XAppDefinition + if r.Type(&a).Where("id = ?", id).First(&a) { + return a, true + } + return xappmodel.XAppDefinition{}, false +} + +// GetByKey 按 key 取(key 上有唯一索引)。 +func (r XAppDefinitionRepo) GetByKey(key string) (xappmodel.XAppDefinition, bool) { + var a xappmodel.XAppDefinition + if r.Type(&a).Where("key = ?", key).First(&a) { + return a, true + } + return xappmodel.XAppDefinition{}, false +} + +// Insert 新建应用定义。 +func (r XAppDefinitionRepo) Insert(a *xappmodel.XAppDefinition) bool { + return r.QueryBuilder.Insert(a) +} + +// Update 更新应用定义。 +func (r XAppDefinitionRepo) Update(a *xappmodel.XAppDefinition) bool { + return r.Save(a) +} + +// Delete 硬删除应用定义。 +func (r XAppDefinitionRepo) Delete(a *xappmodel.XAppDefinition) bool { + return r.QueryBuilder.Delete(a) +} diff --git a/eai_agentplatform/backend-go/internal/skills/api/admin_handlers.go b/eai_agentplatform/backend-go/internal/skills/api/admin_handlers.go index 9d8b137..9b77681 100644 --- a/eai_agentplatform/backend-go/internal/skills/api/admin_handlers.go +++ b/eai_agentplatform/backend-go/internal/skills/api/admin_handlers.go @@ -5,11 +5,19 @@ import ( "github.com/gin-gonic/gin" + "eai_agentplatform/backend/internal/repository" skillmodel "eai_agentplatform/backend/internal/skills/model" - "eai_agentplatform/backend/internal/store" "eai_agentplatform/backend/internal/web" ) +// skillDefinitionRepo 技能定义仓库(便于测试时覆写)。query_handlers.go 里的读接口共用, +// 声明只此一处 —— 两份变量持有同一个仓库时,测试里覆写一份、另一份照旧,行为会静默分叉。 +var skillDefinitionRepo repository.SkillDefinitionRepo + +func init() { + skillDefinitionRepo = repository.SkillDefinitionRepo{} +} + func CreateSkillDefinition(c *gin.Context) { var req definitionReq if err := c.ShouldBindJSON(&req); err != nil { @@ -40,7 +48,7 @@ func CreateSkillDefinition(c *gin.Context) { State: req.State, SortOrder: req.SortOrder, } - if err := store.DB.Create(&item).Error; err != nil { + if !skillDefinitionRepo.Insert(&item) { web.Fail(c, web.NewBadRequest("创建技能定义失败")) return } @@ -53,8 +61,8 @@ func UpdateSkillDefinition(c *gin.Context) { return } - var item skillmodel.SkillDefinition - if err := store.DB.First(&item, id).Error; err != nil { + item, found := skillDefinitionRepo.GetByID(id) + if !found { web.Fail(c, web.NewNotFoundError("技能定义不存在")) return } @@ -87,7 +95,7 @@ func UpdateSkillDefinition(c *gin.Context) { item.State = req.State item.SortOrder = req.SortOrder - if err := store.DB.Save(&item).Error; err != nil { + if !skillDefinitionRepo.Update(&item) { web.Fail(c, web.NewBadRequest("更新技能定义失败")) return } @@ -100,12 +108,12 @@ func DeleteSkillDefinition(c *gin.Context) { return } - var item skillmodel.SkillDefinition - if err := store.DB.First(&item, id).Error; err != nil { + item, found := skillDefinitionRepo.GetByID(id) + if !found { web.Fail(c, web.NewNotFoundError("技能定义不存在")) return } - if err := store.DB.Delete(&item).Error; err != nil { + if !skillDefinitionRepo.Delete(&item) { web.Fail(c, web.NewBadRequest("删除技能定义失败")) return } diff --git a/eai_agentplatform/backend-go/internal/skills/api/query_handlers.go b/eai_agentplatform/backend-go/internal/skills/api/query_handlers.go index ad84ca6..4cc08d0 100644 --- a/eai_agentplatform/backend-go/internal/skills/api/query_handlers.go +++ b/eai_agentplatform/backend-go/internal/skills/api/query_handlers.go @@ -5,28 +5,23 @@ import ( "github.com/gin-gonic/gin" - skillmodel "eai_agentplatform/backend/internal/skills/model" - "eai_agentplatform/backend/internal/store" "eai_agentplatform/backend/internal/web" ) func ListSkillDefinitions(c *gin.Context) { - q := store.DB.Model(&skillmodel.SkillDefinition{}) - if c.Query("state") == "" { - q = q.Where("state = ?", "active") - } else { - q = q.Where("state = ?", c.Query("state")) + // 不传 state 默认只看 active —— 这是列表接口的契约,放在这里解析。 + state := c.Query("state") + if state == "" { + state = "active" } - if c.Query("exposed_to_user") != "" { - q = q.Where("exposed_to_user = ?", c.Query("exposed_to_user") == "true") + // exposed_to_user 三态:不传 = 不过滤,传了按 true/false 精确匹配 + // (原实现是 `!= "true"` 一律当 false,这里保持同一口径)。 + var exposed *bool + if v := c.Query("exposed_to_user"); v != "" { + b := v == "true" + exposed = &b } - - var items []skillmodel.SkillDefinition - if err := q.Order("sort_order ASC, id ASC").Find(&items).Error; err != nil { - web.Fail(c, web.NewBadRequest("查询技能定义失败")) - return - } - web.OK(c, items) + web.OK(c, skillDefinitionRepo.List(state, exposed)) } func GetSkillDefinitionByKey(c *gin.Context) { @@ -36,8 +31,8 @@ func GetSkillDefinitionByKey(c *gin.Context) { return } - var item skillmodel.SkillDefinition - if err := store.DB.Where("key = ?", key).First(&item).Error; err != nil { + item, found := skillDefinitionRepo.GetByKey(key) + if !found { web.Fail(c, web.NewNotFoundError("技能定义不存在")) return } diff --git a/eai_agentplatform/backend-go/internal/xapps/api/handlers.go b/eai_agentplatform/backend-go/internal/xapps/api/handlers.go index 0db4328..2dddb04 100644 --- a/eai_agentplatform/backend-go/internal/xapps/api/handlers.go +++ b/eai_agentplatform/backend-go/internal/xapps/api/handlers.go @@ -2,22 +2,31 @@ package xappapi import ( "encoding/json" - "errors" "fmt" "strconv" "strings" "time" "github.com/gin-gonic/gin" - "gorm.io/gorm" "eai_agentplatform/backend/internal/jsonutil" "eai_agentplatform/backend/internal/middleware" - "eai_agentplatform/backend/internal/store" + "eai_agentplatform/backend/internal/repository" "eai_agentplatform/backend/internal/web" xappdefs "eai_agentplatform/backend/internal/xapps/model" ) +// 应用目录与用户应用中心两个仓库(便于测试时覆写),本包共用。 +var ( + xAppDefinitionRepo repository.XAppDefinitionRepo + userXAppCenterRepo repository.UserXAppCenterRepo +) + +func init() { + xAppDefinitionRepo = repository.XAppDefinitionRepo{} + userXAppCenterRepo = repository.UserXAppCenterRepo{} +} + type definitionReq struct { Key string `json:"key"` Label string `json:"label"` @@ -114,21 +123,19 @@ func validateDefinitionReq(req *definitionReq) *web.AppError { } func ListXAppDefinitions(c *gin.Context) { - q := store.DB.Model(&xappdefs.XAppDefinition{}) - if c.Query("state") == "" { - q = q.Where("state = ?", "active") - } else { - q = q.Where("state = ?", c.Query("state")) + // 不传 state 默认只看 active —— 这是列表接口的契约,放在这里解析。 + state := c.Query("state") + if state == "" { + state = "active" } - if c.Query("exposed_to_user") != "" { - q = q.Where("exposed_to_user = ?", c.Query("exposed_to_user") == "true") + // exposed_to_user 三态:不传 = 不过滤,传了按 true/false 精确匹配 + // (原实现是 `!= "true"` 一律当 false,这里保持同一口径)。 + var exposed *bool + if v := c.Query("exposed_to_user"); v != "" { + b := v == "true" + exposed = &b } - var items []xappdefs.XAppDefinition - if err := q.Order("sort_order ASC, id ASC").Find(&items).Error; err != nil { - web.Fail(c, web.NewBadRequest("查询应用定义失败")) - return - } - web.OK(c, items) + web.OK(c, xAppDefinitionRepo.List(state, exposed)) } func GetXAppDefinitionByKey(c *gin.Context) { @@ -137,8 +144,8 @@ func GetXAppDefinitionByKey(c *gin.Context) { web.Fail(c, web.NewBadRequest("应用 key 不能为空")) return } - var item xappdefs.XAppDefinition - if err := store.DB.Where("key = ?", key).First(&item).Error; err != nil { + item, found := xAppDefinitionRepo.GetByKey(key) + if !found { web.Fail(c, web.NewNotFoundError("应用定义不存在")) return } @@ -180,7 +187,7 @@ func CreateXAppDefinition(c *gin.Context) { State: req.State, SortOrder: req.SortOrder, } - if err := store.DB.Create(&item).Error; err != nil { + if !xAppDefinitionRepo.Insert(&item) { web.Fail(c, web.NewBadRequest("创建应用定义失败")) return } @@ -192,8 +199,8 @@ func UpdateXAppDefinition(c *gin.Context) { if !ok { return } - var item xappdefs.XAppDefinition - if err := store.DB.First(&item, id).Error; err != nil { + item, found := xAppDefinitionRepo.GetByID(id) + if !found { web.Fail(c, web.NewNotFoundError("应用定义不存在")) return } @@ -229,7 +236,7 @@ func UpdateXAppDefinition(c *gin.Context) { item.ExposedToUser = req.ExposedToUser item.State = req.State item.SortOrder = req.SortOrder - if err := store.DB.Save(&item).Error; err != nil { + if !xAppDefinitionRepo.Update(&item) { web.Fail(c, web.NewBadRequest("更新应用定义失败")) return } @@ -241,12 +248,12 @@ func DeleteXAppDefinition(c *gin.Context) { if !ok { return } - var item xappdefs.XAppDefinition - if err := store.DB.First(&item, id).Error; err != nil { + item, found := xAppDefinitionRepo.GetByID(id) + if !found { web.Fail(c, web.NewNotFoundError("应用定义不存在")) return } - if err := store.DB.Delete(&item).Error; err != nil { + if !xAppDefinitionRepo.Delete(&item) { web.Fail(c, web.NewBadRequest("删除应用定义失败")) return } @@ -290,7 +297,7 @@ func GetMyXAppCenter(c *gin.Context) { web.Fail(c, web.NewAuthError("未登录")) return } - row, err := findUserXAppCenter(user.ID) + row, err := userXAppCenterRepo.FindByUser(user.ID) if err != nil { web.Fail(c, web.NewBadRequest("读取应用中心失败")) return @@ -314,7 +321,7 @@ func UpdateMyXAppCenter(c *gin.Context) { RecentKeys: mustJSONXAppCenter(req.RecentKeys), CustomXApps: mustJSONXAppCenter(req.CustomXApps), }) - row, err := findUserXAppCenter(user.ID) + row, err := userXAppCenterRepo.FindByUser(user.ID) if err != nil { web.Fail(c, web.NewBadRequest("读取应用中心失败")) return @@ -325,25 +332,13 @@ func UpdateMyXAppCenter(c *gin.Context) { row.FavoriteKeys = mustJSONXAppCenter(payload.FavoriteKeys) row.RecentKeys = mustJSONXAppCenter(payload.RecentKeys) row.CustomXApps = mustJSONXAppCenter(payload.CustomXApps) - if err := store.DB.Save(row).Error; err != nil { + if !userXAppCenterRepo.Save(row) { web.Fail(c, web.NewBadRequest("保存应用中心失败")) return } web.OK(c, payload) } -func findUserXAppCenter(userID uint) (*xappdefs.UserXAppCenter, error) { - var row xappdefs.UserXAppCenter - err := store.DB.Where("user_id = ?", userID).First(&row).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil - } - if err != nil { - return nil, err - } - return &row, nil -} - func normalizeXAppCenterPayload(row *xappdefs.UserXAppCenter) centerPayload { if row == nil { return centerPayload{