refactor: shorten model and table prefixes
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
- 使用模块路径 `senlinai-agent/backend`。
|
||||
- 业务逻辑包统一放在 `backend/internal/logic` 下,例如 `ai`、`auth`、`files`、`inbox`、`notes`、`projects`、`search`、`tasks`。
|
||||
- Gorm 模型和数据库连接入口统一放在 `backend/internal/models`,不要恢复独立的 `domain` 或 `db` 包。
|
||||
- 模型结构体使用 `SenlinAgent` 前缀,数据库表名使用 `senlin_agent_` 前缀。
|
||||
- 模型结构体使用 `Sa` 前缀,数据库表名使用 `sa_` 前缀。
|
||||
- 所有服务端本地文件路径构造必须集中在文件服务中。
|
||||
- 不允许 HTTP handler 直接构造存储路径。
|
||||
- 项目标签必须限定在项目内;MVP 阶段不要引入全局标签体系。
|
||||
|
||||
@@ -47,7 +47,7 @@ func InitExpert(database *gorm.DB) error {
|
||||
})
|
||||
}
|
||||
|
||||
func initExpertCategories(tx *gorm.DB, experts []expertCatalogItem) (map[string]models.SenlinAgentAIExpertCategory, error) {
|
||||
func initExpertCategories(tx *gorm.DB, experts []expertCatalogItem) (map[string]models.SaAIExpertCategory, error) {
|
||||
categoryNames := make(map[string]string)
|
||||
categoryOrder := make([]string, 0)
|
||||
for _, expert := range experts {
|
||||
@@ -58,9 +58,9 @@ func initExpertCategories(tx *gorm.DB, experts []expertCatalogItem) (map[string]
|
||||
categoryOrder = append(categoryOrder, expert.Category)
|
||||
}
|
||||
|
||||
records := make([]models.SenlinAgentAIExpertCategory, 0, len(categoryOrder))
|
||||
records := make([]models.SaAIExpertCategory, 0, len(categoryOrder))
|
||||
for _, slug := range categoryOrder {
|
||||
records = append(records, models.SenlinAgentAIExpertCategory{
|
||||
records = append(records, models.SaAIExpertCategory{
|
||||
Slug: slug,
|
||||
Name: categoryNames[slug],
|
||||
})
|
||||
@@ -74,11 +74,11 @@ func initExpertCategories(tx *gorm.DB, experts []expertCatalogItem) (map[string]
|
||||
}
|
||||
}
|
||||
|
||||
var stored []models.SenlinAgentAIExpertCategory
|
||||
var stored []models.SaAIExpertCategory
|
||||
if err := tx.Where("slug IN ?", categoryOrder).Find(&stored).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
categories := make(map[string]models.SenlinAgentAIExpertCategory, len(stored))
|
||||
categories := make(map[string]models.SaAIExpertCategory, len(stored))
|
||||
for _, category := range stored {
|
||||
categories[category.Slug] = category
|
||||
}
|
||||
@@ -88,14 +88,14 @@ func initExpertCategories(tx *gorm.DB, experts []expertCatalogItem) (map[string]
|
||||
return categories, nil
|
||||
}
|
||||
|
||||
func initExpertItems(tx *gorm.DB, catalog expertCatalog, categories map[string]models.SenlinAgentAIExpertCategory) error {
|
||||
records := make([]models.SenlinAgentAIExpertItem, 0, len(catalog.Experts))
|
||||
func initExpertItems(tx *gorm.DB, catalog expertCatalog, categories map[string]models.SaAIExpertCategory) error {
|
||||
records := make([]models.SaAIExpertItem, 0, len(catalog.Experts))
|
||||
for _, expert := range catalog.Experts {
|
||||
category, exists := categories[expert.Category]
|
||||
if !exists {
|
||||
return fmt.Errorf("initialize AI expert item %q: category %q not found", expert.Slug, expert.Category)
|
||||
}
|
||||
records = append(records, models.SenlinAgentAIExpertItem{
|
||||
records = append(records, models.SaAIExpertItem{
|
||||
Slug: expert.Slug, CategoryID: category.ID, CategoryIdentity: category.Identity,
|
||||
Category: expert.Category, CategoryName: expert.CategoryName,
|
||||
Name: expert.Name, Description: expert.Description, Emoji: expert.Emoji,
|
||||
|
||||
@@ -16,10 +16,10 @@ func TestNewInitializesUserAndExpertCatalog(t *testing.T) {
|
||||
require.NoError(t, New(database))
|
||||
requireExpertCatalogCounts(t, database)
|
||||
|
||||
var user models.SenlinAgentUser
|
||||
var user models.SaUser
|
||||
require.NoError(t, database.Where("email = ?", rootUsername).First(&user).Error)
|
||||
require.Equal(t, rootRole, user.Role)
|
||||
var expert models.SenlinAgentAIExpertItem
|
||||
var expert models.SaAIExpertItem
|
||||
require.NoError(t, database.Preload("ExpertCategory").Where("slug = ?", "academic-geographer").First(&expert).Error)
|
||||
require.Equal(t, "academic", expert.ExpertCategory.Slug)
|
||||
require.Equal(t, "学术研究", expert.ExpertCategory.Name)
|
||||
@@ -30,7 +30,7 @@ func TestInitExpertOnlyInsertsMissingRecords(t *testing.T) {
|
||||
database := newExpertTestDatabase(t)
|
||||
require.NoError(t, InitExpert(database))
|
||||
|
||||
var preserved models.SenlinAgentAIExpertItem
|
||||
var preserved models.SaAIExpertItem
|
||||
require.NoError(t, database.Where("slug = ?", "academic-geographer").First(&preserved).Error)
|
||||
require.NoError(t, database.Model(&preserved).Updates(map[string]any{
|
||||
"name": "保留名称",
|
||||
@@ -38,7 +38,7 @@ func TestInitExpertOnlyInsertsMissingRecords(t *testing.T) {
|
||||
"category_identity": "",
|
||||
"reference": "",
|
||||
}).Error)
|
||||
require.NoError(t, database.Where("slug = ?", "academic-historian").Delete(&models.SenlinAgentAIExpertItem{}).Error)
|
||||
require.NoError(t, database.Where("slug = ?", "academic-historian").Delete(&models.SaAIExpertItem{}).Error)
|
||||
|
||||
require.NoError(t, InitExpert(database))
|
||||
requireExpertCatalogCounts(t, database)
|
||||
@@ -48,7 +48,7 @@ func TestInitExpertOnlyInsertsMissingRecords(t *testing.T) {
|
||||
require.NotZero(t, preserved.CategoryID)
|
||||
require.NotEmpty(t, preserved.CategoryIdentity)
|
||||
require.Equal(t, "https://ao.aiolaola.com/experts", preserved.Reference)
|
||||
var restored models.SenlinAgentAIExpertItem
|
||||
var restored models.SaAIExpertItem
|
||||
require.NoError(t, database.Where("slug = ?", "academic-historian").First(&restored).Error)
|
||||
}
|
||||
|
||||
@@ -58,9 +58,9 @@ func newExpertTestDatabase(t *testing.T) *gorm.DB {
|
||||
database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, database.AutoMigrate(
|
||||
&models.SenlinAgentUser{},
|
||||
&models.SenlinAgentAIExpertCategory{},
|
||||
&models.SenlinAgentAIExpertItem{},
|
||||
&models.SaUser{},
|
||||
&models.SaAIExpertCategory{},
|
||||
&models.SaAIExpertItem{},
|
||||
))
|
||||
return database
|
||||
}
|
||||
@@ -69,9 +69,9 @@ func requireExpertCatalogCounts(t *testing.T, database *gorm.DB) {
|
||||
t.Helper()
|
||||
|
||||
var categoryCount int64
|
||||
require.NoError(t, database.Model(&models.SenlinAgentAIExpertCategory{}).Count(&categoryCount).Error)
|
||||
require.NoError(t, database.Model(&models.SaAIExpertCategory{}).Count(&categoryCount).Error)
|
||||
require.Equal(t, int64(19), categoryCount)
|
||||
var expertCount int64
|
||||
require.NoError(t, database.Model(&models.SenlinAgentAIExpertItem{}).Count(&expertCount).Error)
|
||||
require.NoError(t, database.Model(&models.SaAIExpertItem{}).Count(&expertCount).Error)
|
||||
require.Equal(t, int64(267), expertCount)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const (
|
||||
// InitUser creates the default root account when the user table is empty.
|
||||
func InitUser(database *gorm.DB) error {
|
||||
var count int64
|
||||
if err := database.Model(&models.SenlinAgentUser{}).Count(&count).Error; err != nil {
|
||||
if err := database.Model(&models.SaUser{}).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
@@ -27,7 +27,7 @@ func InitUser(database *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return database.Create(&models.SenlinAgentUser{
|
||||
return database.Create(&models.SaUser{
|
||||
Email: rootUsername,
|
||||
DisplayName: rootUsername,
|
||||
PasswordHash: string(passwordHash),
|
||||
|
||||
@@ -15,7 +15,7 @@ func TestInitUserCreatesRootUserWhenTableIsEmpty(t *testing.T) {
|
||||
|
||||
require.NoError(t, InitUser(database))
|
||||
|
||||
var users []models.SenlinAgentUser
|
||||
var users []models.SaUser
|
||||
require.NoError(t, database.Find(&users).Error)
|
||||
require.Len(t, users, 1)
|
||||
require.Equal(t, rootUsername, users[0].Email)
|
||||
@@ -26,7 +26,7 @@ func TestInitUserCreatesRootUserWhenTableIsEmpty(t *testing.T) {
|
||||
|
||||
func TestInitUserDoesNothingWhenTableIsNotEmpty(t *testing.T) {
|
||||
database := newUserTestDatabase(t)
|
||||
existing := models.SenlinAgentUser{
|
||||
existing := models.SaUser{
|
||||
Email: "existing@example.com",
|
||||
DisplayName: "Existing User",
|
||||
PasswordHash: "existing-hash",
|
||||
@@ -36,7 +36,7 @@ func TestInitUserDoesNothingWhenTableIsNotEmpty(t *testing.T) {
|
||||
|
||||
require.NoError(t, InitUser(database))
|
||||
|
||||
var users []models.SenlinAgentUser
|
||||
var users []models.SaUser
|
||||
require.NoError(t, database.Find(&users).Error)
|
||||
require.Len(t, users, 1)
|
||||
require.Equal(t, existing.Email, users[0].Email)
|
||||
@@ -47,6 +47,6 @@ func newUserTestDatabase(t *testing.T) *gorm.DB {
|
||||
|
||||
database, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, database.AutoMigrate(&models.SenlinAgentUser{}))
|
||||
require.NoError(t, database.AutoMigrate(&models.SaUser{}))
|
||||
return database
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ type ExpertFilter struct {
|
||||
Query string
|
||||
}
|
||||
|
||||
func (s *SessionService) ListExperts(filter ExpertFilter) ([]models.SenlinAgentAIExpertItem, error) {
|
||||
query := models.DBService.Model(&models.SenlinAgentAIExpertItem{}).Where("enabled = ?", true)
|
||||
func (s *SessionService) ListExperts(filter ExpertFilter) ([]models.SaAIExpertItem, error) {
|
||||
query := models.DBService.Model(&models.SaAIExpertItem{}).Where("enabled = ?", true)
|
||||
if category := strings.TrimSpace(filter.Category); category != "" {
|
||||
query = query.Where("category = ?", category)
|
||||
}
|
||||
@@ -26,18 +26,18 @@ func (s *SessionService) ListExperts(filter ExpertFilter) ([]models.SenlinAgentA
|
||||
like, like, like,
|
||||
)
|
||||
}
|
||||
var experts []models.SenlinAgentAIExpertItem
|
||||
var experts []models.SaAIExpertItem
|
||||
if err := query.Order("category asc, id asc").Find(&experts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if experts == nil {
|
||||
experts = []models.SenlinAgentAIExpertItem{}
|
||||
experts = []models.SaAIExpertItem{}
|
||||
}
|
||||
return experts, nil
|
||||
}
|
||||
|
||||
func (s *SessionService) GetExpert(identity string) (*models.SenlinAgentAIExpertItem, error) {
|
||||
var expert models.SenlinAgentAIExpertItem
|
||||
func (s *SessionService) GetExpert(identity string) (*models.SaAIExpertItem, error) {
|
||||
var expert models.SaAIExpertItem
|
||||
if err := models.DBService.Where("identity = ? AND enabled = ?", strings.TrimSpace(identity), true).First(&expert).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrExpertNotFound
|
||||
@@ -47,11 +47,11 @@ func (s *SessionService) GetExpert(identity string) (*models.SenlinAgentAIExpert
|
||||
return &expert, nil
|
||||
}
|
||||
|
||||
func findExpertByIdentity(tx *gorm.DB, identity string) (*models.SenlinAgentAIExpertItem, error) {
|
||||
func findExpertByIdentity(tx *gorm.DB, identity string) (*models.SaAIExpertItem, error) {
|
||||
if strings.TrimSpace(identity) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var expert models.SenlinAgentAIExpertItem
|
||||
var expert models.SaAIExpertItem
|
||||
if err := tx.Where("identity = ? AND enabled = ?", identity, true).First(&expert).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrExpertNotFound
|
||||
|
||||
@@ -45,7 +45,7 @@ func (g *Gateway) SaveUserKey(userID uint, provider string, apiKey string) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := models.SenlinAgentAIKey{UserID: userID, Provider: provider, EncryptedAPIKey: encrypted}
|
||||
key := models.SaAIKey{UserID: userID, Provider: provider, EncryptedAPIKey: encrypted}
|
||||
return models.DBService.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "user_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"provider", "encrypted_api_key", "updated_at"}),
|
||||
@@ -53,7 +53,7 @@ func (g *Gateway) SaveUserKey(userID uint, provider string, apiKey string) error
|
||||
}
|
||||
|
||||
func (g *Gateway) SelectKey(userID uint) (SelectedKey, error) {
|
||||
var userKey models.SenlinAgentAIKey
|
||||
var userKey models.SaAIKey
|
||||
err := models.DBService.Where("user_id = ?", userID).First(&userKey).Error
|
||||
if err == nil {
|
||||
selected := SelectedKey{Provider: userKey.Provider, KeyType: "user"}
|
||||
@@ -77,7 +77,7 @@ func (g *Gateway) RecordCall(database *gorm.DB, userID uint, provider string, us
|
||||
if database == nil {
|
||||
database = models.DBService
|
||||
}
|
||||
return database.Create(&models.SenlinAgentAICallLog{
|
||||
return database.Create(&models.SaAICallLog{
|
||||
UserID: userID,
|
||||
Provider: provider,
|
||||
UsedKeyType: usedKeyType,
|
||||
@@ -98,17 +98,17 @@ func (g *Gateway) ReserveRateLimit(userID uint, action string, limit int, window
|
||||
currentTime = g.now().UTC()
|
||||
}
|
||||
windowStart := currentTime.Truncate(window)
|
||||
bucket := models.SenlinAgentAIRateBucket{
|
||||
bucket := models.SaAIRateBucket{
|
||||
UserID: userID, Action: action, WindowStart: windowStart, Count: 1,
|
||||
}
|
||||
result := models.DBService.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "user_id"}, {Name: "action"}, {Name: "window_start"}},
|
||||
DoUpdates: clause.Assignments(map[string]any{
|
||||
"count": gorm.Expr("senlin_agent_ai_rate_buckets.count + 1"),
|
||||
"count": gorm.Expr("sa_ai_rate_buckets.count + 1"),
|
||||
"updated_at": currentTime,
|
||||
}),
|
||||
Where: clause.Where{Exprs: []clause.Expression{
|
||||
clause.Lt{Column: clause.Column{Table: "senlin_agent_ai_rate_buckets", Name: "count"}, Value: limit},
|
||||
clause.Lt{Column: clause.Column{Table: "sa_ai_rate_buckets", Name: "count"}, Value: limit},
|
||||
}},
|
||||
}).Create(&bucket)
|
||||
if result.Error != nil {
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
func TestSelectKeyPrefersUserKey(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentAIKey{UserID: 3, Provider: "openai", EncryptedAPIKey: "user-key"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaAIKey{UserID: 3, Provider: "openai", EncryptedAPIKey: "user-key"}).Error)
|
||||
gateway := NewGateway("system-key")
|
||||
|
||||
selected, err := gateway.SelectKey(3)
|
||||
@@ -30,7 +30,7 @@ func TestSaveUserKeyEncryptsStoredKey(t *testing.T) {
|
||||
|
||||
require.NoError(t, gateway.SaveUserKey(3, "openai", "user-key"))
|
||||
|
||||
var stored models.SenlinAgentAIKey
|
||||
var stored models.SaAIKey
|
||||
require.NoError(t, database.Where("user_id = ?", 3).First(&stored).Error)
|
||||
require.NotEqual(t, "user-key", stored.EncryptedAPIKey)
|
||||
require.Contains(t, stored.EncryptedAPIKey, "v1:")
|
||||
@@ -65,7 +65,7 @@ func TestRecordCallStoresAuditFields(t *testing.T) {
|
||||
|
||||
require.NoError(t, gateway.RecordCall(database, 3, "openai", "system", "inbox_analyze", "failed", "rate limited"))
|
||||
|
||||
var log models.SenlinAgentAICallLog
|
||||
var log models.SaAICallLog
|
||||
require.NoError(t, database.First(&log).Error)
|
||||
require.Equal(t, uint(3), log.UserID)
|
||||
require.Equal(t, "openai", log.Provider)
|
||||
@@ -77,7 +77,7 @@ func TestRecordCallStoresAuditFields(t *testing.T) {
|
||||
|
||||
func TestReserveRateLimitRejectsCallsOverWindow(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "rate@example.com", DisplayName: "Rate", PasswordHash: "hash"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "rate@example.com", DisplayName: "Rate", PasswordHash: "hash"}).Error)
|
||||
gateway := NewGateway("system-key")
|
||||
require.NoError(t, gateway.ReserveRateLimit(1, "inbox_analyze", 1, time.Hour))
|
||||
|
||||
@@ -88,9 +88,9 @@ func TestReserveRateLimitRejectsCallsOverWindow(t *testing.T) {
|
||||
|
||||
func TestCreateAISession(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
user := models.SenlinAgentUser{Email: "session@example.com", DisplayName: "Session User", PasswordHash: "hash"}
|
||||
user := models.SaUser{Email: "session@example.com", DisplayName: "Session User", PasswordHash: "hash"}
|
||||
require.NoError(t, database.Create(&user).Error)
|
||||
project := models.SenlinAgentProject{OwnerID: user.ID, Name: "Session Project", Identifier: "SESSION"}
|
||||
project := models.SaProject{OwnerID: user.ID, Name: "Session Project", Identifier: "SESSION"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
service := NewSessionService(NewGateway("system-key"))
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ func aiRequestContext(c *gin.Context) (uint, string, bool) {
|
||||
return userID, projectIdentity, true
|
||||
}
|
||||
|
||||
func sessionDTO(session models.SenlinAgentAISession) SessionDTO {
|
||||
func sessionDTO(session models.SaAISession) SessionDTO {
|
||||
dto := SessionDTO{
|
||||
ID: session.Identity,
|
||||
ProjectID: session.ProjectIdentity,
|
||||
@@ -158,12 +158,12 @@ func sessionDTO(session models.SenlinAgentAISession) SessionDTO {
|
||||
return dto
|
||||
}
|
||||
|
||||
func expertSummaryPointer(expert models.SenlinAgentAIExpertItem) *ExpertSummaryDTO {
|
||||
func expertSummaryPointer(expert models.SaAIExpertItem) *ExpertSummaryDTO {
|
||||
dto := expertSummaryDTO(expert)
|
||||
return &dto
|
||||
}
|
||||
|
||||
func expertSummaryDTO(expert models.SenlinAgentAIExpertItem) ExpertSummaryDTO {
|
||||
func expertSummaryDTO(expert models.SaAIExpertItem) ExpertSummaryDTO {
|
||||
return ExpertSummaryDTO{
|
||||
ID: expert.Identity, Slug: expert.Slug, Category: expert.Category,
|
||||
CategoryName: expert.CategoryName, Name: expert.Name, Description: expert.Description,
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestAISessionHandlersRequireOwnedProject(t *testing.T) {
|
||||
}
|
||||
|
||||
var count int64
|
||||
require.NoError(t, database.Model(&models.SenlinAgentAISession{}).Count(&count).Error)
|
||||
require.NoError(t, database.Model(&models.SaAISession{}).Count(&count).Error)
|
||||
require.Zero(t, count)
|
||||
require.Empty(t, gateway.steps)
|
||||
}
|
||||
@@ -55,7 +55,7 @@ func TestAIExpertLibraryListsDetailsAndCreatesAssociatedSession(t *testing.T) {
|
||||
database := newAIHandlerTestDB(t)
|
||||
owner := createAIHandlerUser(t, database, "expert-owner@example.com")
|
||||
project := createAIHandlerProject(t, database, owner.ID, "EXPERTS")
|
||||
var expert models.SenlinAgentAIExpertItem
|
||||
var expert models.SaAIExpertItem
|
||||
require.NoError(t, database.Order("id asc").First(&expert).Error)
|
||||
router := aiHandlerTestRouter(owner.ID, NewGatewayWithSecret("system-key", "test-encryption-secret"))
|
||||
|
||||
@@ -87,7 +87,7 @@ func TestAIExpertLibraryListsDetailsAndCreatesAssociatedSession(t *testing.T) {
|
||||
responseExpert := createPayload["expert"].(map[string]any)
|
||||
require.Equal(t, expert.Identity, responseExpert["id"])
|
||||
require.Equal(t, expert.Name, responseExpert["name"])
|
||||
var session models.SenlinAgentAISession
|
||||
var session models.SaAISession
|
||||
require.NoError(t, database.First(&session).Error)
|
||||
require.NotNil(t, session.ExpertID)
|
||||
require.Equal(t, expert.ID, *session.ExpertID)
|
||||
@@ -131,7 +131,7 @@ func TestCreateAISessionReturnsRateLimitBeforeMissingKeyAndAuditsFailure(t *test
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &payload))
|
||||
require.Equal(t, "ai_rate_limited", payload.Error.Code)
|
||||
require.Equal(t, "AI 请求过于频繁,请稍后重试", payload.Error.Message)
|
||||
var latest models.SenlinAgentAICallLog
|
||||
var latest models.SaAICallLog
|
||||
require.NoError(t, database.Order("id desc").First(&latest).Error)
|
||||
require.Equal(t, "none", latest.Provider)
|
||||
require.Equal(t, "none", latest.UsedKeyType)
|
||||
@@ -157,16 +157,16 @@ func TestCreateAISessionWithoutKeyReturnsAuditedErrorAndCreatesNoFormalObjects(t
|
||||
require.Equal(t, "ai_key_missing", payload.Error.Code)
|
||||
require.Equal(t, "尚未配置可用的 AI 密钥", payload.Error.Message)
|
||||
for _, model := range []any{
|
||||
&models.SenlinAgentAISession{},
|
||||
&models.SenlinAgentTask{},
|
||||
&models.SenlinAgentNote{},
|
||||
&models.SenlinAgentSource{},
|
||||
&models.SaAISession{},
|
||||
&models.SaTask{},
|
||||
&models.SaNote{},
|
||||
&models.SaSource{},
|
||||
} {
|
||||
var count int64
|
||||
require.NoError(t, database.Model(model).Count(&count).Error)
|
||||
require.Zero(t, count)
|
||||
}
|
||||
var call models.SenlinAgentAICallLog
|
||||
var call models.SaAICallLog
|
||||
require.NoError(t, database.First(&call).Error)
|
||||
require.Equal(t, owner.ID, call.UserID)
|
||||
require.Equal(t, "none", call.Provider)
|
||||
@@ -174,7 +174,7 @@ func TestCreateAISessionWithoutKeyReturnsAuditedErrorAndCreatesNoFormalObjects(t
|
||||
require.Equal(t, "ai_session_create", call.Action)
|
||||
require.Equal(t, "failed", call.Status)
|
||||
require.Equal(t, "ai_key_missing", call.Error)
|
||||
var bucket models.SenlinAgentAIRateBucket
|
||||
var bucket models.SaAIRateBucket
|
||||
require.NoError(t, database.Where("user_id = ? AND action = ?", owner.ID, aiSessionCreateAction).First(&bucket).Error)
|
||||
require.Equal(t, 1, bucket.Count)
|
||||
}
|
||||
@@ -186,7 +186,7 @@ func TestCreateAISessionRollsBackSessionWhenReadyAuditWriteFails(t *testing.T) {
|
||||
injectedError := errors.New("injected ready audit failure")
|
||||
callbackName := "test:fail_ready_ai_audit"
|
||||
require.NoError(t, database.Callback().Create().Before("gorm:create").Register(callbackName, func(tx *gorm.DB) {
|
||||
call, ok := tx.Statement.Dest.(*models.SenlinAgentAICallLog)
|
||||
call, ok := tx.Statement.Dest.(*models.SaAICallLog)
|
||||
if ok && call.Status == defaultSessionStatus {
|
||||
tx.AddError(injectedError)
|
||||
}
|
||||
@@ -201,9 +201,9 @@ func TestCreateAISessionRollsBackSessionWhenReadyAuditWriteFails(t *testing.T) {
|
||||
|
||||
require.Equal(t, http.StatusInternalServerError, recorder.Code)
|
||||
var sessionCount int64
|
||||
require.NoError(t, database.Model(&models.SenlinAgentAISession{}).Where("project_id = ?", project.ID).Count(&sessionCount).Error)
|
||||
require.NoError(t, database.Model(&models.SaAISession{}).Where("project_id = ?", project.ID).Count(&sessionCount).Error)
|
||||
require.Zero(t, sessionCount)
|
||||
var calls []models.SenlinAgentAICallLog
|
||||
var calls []models.SaAICallLog
|
||||
require.NoError(t, database.Where("user_id = ? AND action = ?", owner.ID, aiSessionCreateAction).Find(&calls).Error)
|
||||
require.Len(t, calls, 1)
|
||||
require.Equal(t, "openai", calls[0].Provider)
|
||||
@@ -216,7 +216,7 @@ func TestCreateAISessionAuditsKnownProviderMetadataWhenUserKeyDecryptFails(t *te
|
||||
database := newAIHandlerTestDB(t)
|
||||
owner := createAIHandlerUser(t, database, "decrypt-failure@example.com")
|
||||
project := createAIHandlerProject(t, database, owner.ID, "DECRYPT_FAILURE")
|
||||
require.NoError(t, database.Create(&models.SenlinAgentAIKey{
|
||||
require.NoError(t, database.Create(&models.SaAIKey{
|
||||
UserID: owner.ID, Provider: "deepseek", EncryptedAPIKey: "v1:not-valid-base64",
|
||||
}).Error)
|
||||
router := aiHandlerTestRouter(owner.ID, NewGatewayWithSecret("system-key", "test-encryption-secret"))
|
||||
@@ -227,7 +227,7 @@ func TestCreateAISessionAuditsKnownProviderMetadataWhenUserKeyDecryptFails(t *te
|
||||
}))
|
||||
|
||||
require.Equal(t, http.StatusInternalServerError, recorder.Code)
|
||||
var call models.SenlinAgentAICallLog
|
||||
var call models.SaAICallLog
|
||||
require.NoError(t, database.Where("user_id = ? AND action = ?", owner.ID, aiSessionCreateAction).First(&call).Error)
|
||||
require.Equal(t, "deepseek", call.Provider)
|
||||
require.Equal(t, "user", call.UsedKeyType)
|
||||
@@ -235,7 +235,7 @@ func TestCreateAISessionAuditsKnownProviderMetadataWhenUserKeyDecryptFails(t *te
|
||||
require.Equal(t, "provider_selection_failed", call.Error)
|
||||
require.NotContains(t, call.Error, "not-valid-base64")
|
||||
var sessionCount int64
|
||||
require.NoError(t, database.Model(&models.SenlinAgentAISession{}).Count(&sessionCount).Error)
|
||||
require.NoError(t, database.Model(&models.SaAISession{}).Count(&sessionCount).Error)
|
||||
require.Zero(t, sessionCount)
|
||||
}
|
||||
|
||||
@@ -266,14 +266,14 @@ func TestCreateAISessionReturnsIdentityDTOAndCompleteAuditWithoutAutomaticObject
|
||||
require.NotContains(t, payload, forbidden)
|
||||
}
|
||||
|
||||
var call models.SenlinAgentAICallLog
|
||||
var call models.SaAICallLog
|
||||
require.NoError(t, database.First(&call).Error)
|
||||
require.Equal(t, "openai", call.Provider)
|
||||
require.Equal(t, "system", call.UsedKeyType)
|
||||
require.Equal(t, "ai_session_create", call.Action)
|
||||
require.Equal(t, "ready", call.Status)
|
||||
require.Empty(t, call.Error)
|
||||
for _, model := range []any{&models.SenlinAgentTask{}, &models.SenlinAgentNote{}, &models.SenlinAgentSource{}} {
|
||||
for _, model := range []any{&models.SaTask{}, &models.SaNote{}, &models.SaSource{}} {
|
||||
var count int64
|
||||
require.NoError(t, database.Model(model).Count(&count).Error)
|
||||
require.Zero(t, count)
|
||||
@@ -285,10 +285,10 @@ func TestListAISessionsReturnsOnlyOwnedProjectIdentityDTOs(t *testing.T) {
|
||||
owner := createAIHandlerUser(t, database, "owner@example.com")
|
||||
project := createAIHandlerProject(t, database, owner.ID, "LIST")
|
||||
otherProject := createAIHandlerProject(t, database, owner.ID, "OTHER")
|
||||
require.NoError(t, database.Create(&models.SenlinAgentAISession{
|
||||
require.NoError(t, database.Create(&models.SaAISession{
|
||||
ProjectID: project.ID, CreatedBy: owner.ID, Title: "目标会话", Context: "项目上下文",
|
||||
}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentAISession{
|
||||
require.NoError(t, database.Create(&models.SaAISession{
|
||||
ProjectID: otherProject.ID, CreatedBy: owner.ID, Title: "其他会话", Context: "不得混入",
|
||||
}).Error)
|
||||
router := aiHandlerTestRouter(owner.ID, &recordingSessionGateway{})
|
||||
@@ -339,16 +339,16 @@ func newAIHandlerTestDB(t *testing.T) *gorm.DB {
|
||||
return database
|
||||
}
|
||||
|
||||
func createAIHandlerUser(t *testing.T, database *gorm.DB, email string) models.SenlinAgentUser {
|
||||
func createAIHandlerUser(t *testing.T, database *gorm.DB, email string) models.SaUser {
|
||||
t.Helper()
|
||||
user := models.SenlinAgentUser{Email: email, DisplayName: email, PasswordHash: "hash"}
|
||||
user := models.SaUser{Email: email, DisplayName: email, PasswordHash: "hash"}
|
||||
require.NoError(t, database.Create(&user).Error)
|
||||
return user
|
||||
}
|
||||
|
||||
func createAIHandlerProject(t *testing.T, database *gorm.DB, ownerID uint, identifier string) models.SenlinAgentProject {
|
||||
func createAIHandlerProject(t *testing.T, database *gorm.DB, ownerID uint, identifier string) models.SaProject {
|
||||
t.Helper()
|
||||
project := models.SenlinAgentProject{OwnerID: ownerID, Name: identifier, Identifier: identifier}
|
||||
project := models.SaProject{OwnerID: ownerID, Name: identifier, Identifier: identifier}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
return project
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestPostgresReserveRateLimitIsAtomicAcrossConcurrentConnections(t *testing.
|
||||
user := createAIRateTestUser(t, database, "postgres-rate-"+suffix+"@example.com")
|
||||
action := "postgres_concurrent_" + suffix
|
||||
t.Cleanup(func() {
|
||||
database.Where("user_id = ?", user.ID).Delete(&models.SenlinAgentAIRateBucket{})
|
||||
database.Where("user_id = ?", user.ID).Delete(&models.SaAIRateBucket{})
|
||||
database.Delete(&user)
|
||||
})
|
||||
gateway := NewGatewayWithSecret("system-key", "test-encryption-secret")
|
||||
@@ -66,7 +66,7 @@ func TestPostgresReserveRateLimitIsAtomicAcrossConcurrentConnections(t *testing.
|
||||
}
|
||||
require.Equal(t, limit, allowed)
|
||||
require.Equal(t, attempts-limit, limited)
|
||||
var bucket models.SenlinAgentAIRateBucket
|
||||
var bucket models.SaAIRateBucket
|
||||
require.NoError(t, database.Where("user_id = ? AND action = ?", user.ID, action).First(&bucket).Error)
|
||||
require.Equal(t, limit, bucket.Count)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func TestReserveRateLimitIsAtomicUnderConcurrentSQLiteRequests(t *testing.T) {
|
||||
}
|
||||
require.Equal(t, limit, allowed)
|
||||
require.Equal(t, attempts-limit, limited)
|
||||
var buckets []models.SenlinAgentAIRateBucket
|
||||
var buckets []models.SaAIRateBucket
|
||||
require.NoError(t, database.Where("user_id = ? AND action = ?", user.ID, "concurrent_session_create").Find(&buckets).Error)
|
||||
require.Len(t, buckets, 1)
|
||||
require.Equal(t, limit, buckets[0].Count)
|
||||
@@ -72,7 +72,7 @@ func TestReserveRateLimitUsesFixedWindowsAndCountsFailedAttempts(t *testing.T) {
|
||||
|
||||
current = current.Add(time.Hour)
|
||||
require.NoError(t, gateway.ReserveRateLimit(user.ID, "windowed_session_create", 2, time.Hour))
|
||||
var buckets []models.SenlinAgentAIRateBucket
|
||||
var buckets []models.SaAIRateBucket
|
||||
require.NoError(t, database.Where("user_id = ? AND action = ?", user.ID, "windowed_session_create").Order("window_start asc").Find(&buckets).Error)
|
||||
require.Len(t, buckets, 2)
|
||||
require.Equal(t, []int{2, 1}, []int{buckets[0].Count, buckets[1].Count})
|
||||
@@ -93,9 +93,9 @@ func newConcurrentAIRateTestDB(t *testing.T) *gorm.DB {
|
||||
return database
|
||||
}
|
||||
|
||||
func createAIRateTestUser(t *testing.T, database *gorm.DB, email string) models.SenlinAgentUser {
|
||||
func createAIRateTestUser(t *testing.T, database *gorm.DB, email string) models.SaUser {
|
||||
t.Helper()
|
||||
user := models.SenlinAgentUser{Email: email, DisplayName: email, PasswordHash: "hash"}
|
||||
user := models.SaUser{Email: email, DisplayName: email, PasswordHash: "hash"}
|
||||
require.NoError(t, database.Create(&user).Error)
|
||||
return user
|
||||
}
|
||||
|
||||
@@ -38,28 +38,28 @@ func NewSessionService(gateway sessionGateway) *SessionService {
|
||||
}
|
||||
|
||||
// List 在项目 owner 校验后返回该项目的会话,内部自增 ID 不离开服务边界。
|
||||
func (s *SessionService) List(userID uint, projectIdentity string) ([]models.SenlinAgentAISession, error) {
|
||||
func (s *SessionService) List(userID uint, projectIdentity string) ([]models.SaAISession, error) {
|
||||
project, err := projects.FindOwnedProject(userID, projectIdentity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sessions []models.SenlinAgentAISession
|
||||
var sessions []models.SaAISession
|
||||
if err := models.DBService.Preload("Expert").Where("project_id = ?", project.ID).Order("updated_at desc, id desc").Find(&sessions).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sessions == nil {
|
||||
sessions = []models.SenlinAgentAISession{}
|
||||
sessions = []models.SaAISession{}
|
||||
}
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
// Create 先校验项目,再限流,最后才选择 provider/key;会话创建不会生成任何正式业务对象。
|
||||
func (s *SessionService) Create(userID uint, projectIdentity, title, context string) (*models.SenlinAgentAISession, error) {
|
||||
func (s *SessionService) Create(userID uint, projectIdentity, title, context string) (*models.SaAISession, error) {
|
||||
return s.CreateWithExpert(userID, projectIdentity, title, context, "")
|
||||
}
|
||||
|
||||
// CreateWithExpert 创建带本地专家角色的项目会话。
|
||||
func (s *SessionService) CreateWithExpert(userID uint, projectIdentity, title, context, expertIdentity string) (*models.SenlinAgentAISession, error) {
|
||||
func (s *SessionService) CreateWithExpert(userID uint, projectIdentity, title, context, expertIdentity string) (*models.SaAISession, error) {
|
||||
title = strings.TrimSpace(title)
|
||||
context = strings.TrimSpace(context)
|
||||
if title == "" {
|
||||
@@ -103,7 +103,7 @@ func (s *SessionService) CreateWithExpert(userID uint, projectIdentity, title, c
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session := models.SenlinAgentAISession{
|
||||
session := models.SaAISession{
|
||||
ProjectID: project.ID,
|
||||
CreatedBy: userID,
|
||||
Title: title,
|
||||
@@ -149,7 +149,7 @@ func selectedAuditMetadata(selected SelectedKey) (string, string) {
|
||||
return provider, keyType
|
||||
}
|
||||
|
||||
func aiSessionStatus(session models.SenlinAgentAISession) string {
|
||||
func aiSessionStatus(session models.SaAISession) string {
|
||||
if status := strings.TrimSpace(session.Status); status != "" {
|
||||
return status
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func TestLoginHandlerReturnsSessionToken(t *testing.T) {
|
||||
func TestLoginHandlerRejectsInvalidCredentials(t *testing.T) {
|
||||
newTestDB(t)
|
||||
service := auth.NewService("test-secret")
|
||||
require.NoError(t, models.DBService.Create(&models.SenlinAgentUser{Email: "demo@senlin.ai", DisplayName: "Demo User", PasswordHash: "not-a-bcrypt-hash"}).Error)
|
||||
require.NoError(t, models.DBService.Create(&models.SaUser{Email: "demo@senlin.ai", DisplayName: "Demo User", PasswordHash: "not-a-bcrypt-hash"}).Error)
|
||||
router := httpx.NewProtectedRouter(config.Config{Env: "test"}, service.VerifySession, auth.NewHandler(service))
|
||||
|
||||
body, err := json.Marshal(gin.H{"email": "demo@senlin.ai", "password": "wrong"})
|
||||
|
||||
@@ -38,7 +38,7 @@ func (s *Service) CreateInvite(adminID uint, email string) (string, error) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) RegisterWithInvite(token, email, displayName, password string) (*models.SenlinAgentUser, error) {
|
||||
func (s *Service) RegisterWithInvite(token, email, displayName, password string) (*models.SaUser, error) {
|
||||
payload, err := s.verifyToken(token, "invite")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -50,7 +50,7 @@ func (s *Service) RegisterWithInvite(token, email, displayName, password string)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := &models.SenlinAgentUser{
|
||||
user := &models.SaUser{
|
||||
Email: payload.Subject,
|
||||
DisplayName: strings.TrimSpace(displayName),
|
||||
PasswordHash: string(hash),
|
||||
@@ -60,7 +60,7 @@ func (s *Service) RegisterWithInvite(token, email, displayName, password string)
|
||||
}
|
||||
|
||||
func (s *Service) Login(email, password string) (string, error) {
|
||||
var user models.SenlinAgentUser
|
||||
var user models.SaUser
|
||||
if err := models.DBService.Where("email = ?", strings.ToLower(strings.TrimSpace(email))).First(&user).Error; err != nil {
|
||||
return "", errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ func (h *Handler) upload(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, sourceDTO(*source))
|
||||
}
|
||||
|
||||
func sourceDTO(source models.SenlinAgentSource) SourceDTO {
|
||||
func sourceDTO(source models.SaSource) SourceDTO {
|
||||
return SourceDTO{
|
||||
ID: source.Identity, ProjectID: source.ProjectIdentity, Kind: source.Kind,
|
||||
Title: source.Title, StorageKey: filepath.Base(filepath.FromSlash(source.FilePath)),
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestFileRegistrarSavesBeforeCreatingIdentitySourceDTO(t *testing.T) {
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusCreated, rec.Code, rec.Body.String())
|
||||
var source models.SenlinAgentSource
|
||||
var source models.SaSource
|
||||
require.NoError(t, database.Where("project_id = ?", project.ID).First(&source).Error)
|
||||
require.FileExists(t, filepath.Join(storageRoot, filepath.FromSlash(source.FilePath)))
|
||||
require.Contains(t, filepath.ToSlash(source.FilePath), "projects/"+project.Identity+"/")
|
||||
@@ -75,7 +75,7 @@ func TestFileRegistrarChecksOwnershipBeforeWritingFile(t *testing.T) {
|
||||
|
||||
require.Equal(t, http.StatusNotFound, rec.Code, rec.Body.String())
|
||||
var count int64
|
||||
require.NoError(t, database.Model(&models.SenlinAgentSource{}).Count(&count).Error)
|
||||
require.NoError(t, database.Model(&models.SaSource{}).Count(&count).Error)
|
||||
require.Zero(t, count)
|
||||
entries, err := os.ReadDir(storageRoot)
|
||||
require.NoError(t, err)
|
||||
@@ -85,7 +85,7 @@ func TestFileRegistrarChecksOwnershipBeforeWritingFile(t *testing.T) {
|
||||
func TestFileRegistrarRemovesStoredFileWhenSourceCreateFails(t *testing.T) {
|
||||
router, database, project, storageRoot := newFileHandlerTestRouter(t, 1, 1)
|
||||
require.NoError(t, database.Callback().Create().Before("gorm:create").Register("test:fail_source_create", func(tx *gorm.DB) {
|
||||
if tx.Statement.Schema != nil && tx.Statement.Schema.Table == (models.SenlinAgentSource{}).TableName() {
|
||||
if tx.Statement.Schema != nil && tx.Statement.Schema.Table == (models.SaSource{}).TableName() {
|
||||
tx.AddError(errors.New("forced source create failure"))
|
||||
}
|
||||
}))
|
||||
@@ -139,7 +139,7 @@ func TestFileRegistrarRejectsRequestOverConfiguredUploadLimit(t *testing.T) {
|
||||
func TestFileRegistrarLogsCleanupFailureWithoutLeakingStorageRoot(t *testing.T) {
|
||||
router, database, project, storageRoot, service := newFileHandlerTestRouterWithService(t, 1, 1, DefaultMaxUploadBytes)
|
||||
require.NoError(t, database.Callback().Create().Before("gorm:create").Register("test:fail_source_create_for_cleanup_log", func(tx *gorm.DB) {
|
||||
if tx.Statement.Schema != nil && tx.Statement.Schema.Table == (models.SenlinAgentSource{}).TableName() {
|
||||
if tx.Statement.Schema != nil && tx.Statement.Schema.Table == (models.SaSource{}).TableName() {
|
||||
tx.AddError(errors.New("forced source create failure"))
|
||||
}
|
||||
}))
|
||||
@@ -173,24 +173,24 @@ func TestFileRegistrarLogsCleanupFailureWithoutLeakingStorageRoot(t *testing.T)
|
||||
require.Contains(t, serverLog.String(), storageRoot)
|
||||
}
|
||||
|
||||
func newFileHandlerTestRouter(t *testing.T, currentUserID, ownerID uint) (*gin.Engine, *gorm.DB, models.SenlinAgentProject, string) {
|
||||
func newFileHandlerTestRouter(t *testing.T, currentUserID, ownerID uint) (*gin.Engine, *gorm.DB, models.SaProject, string) {
|
||||
return newFileHandlerTestRouterWithLimit(t, currentUserID, ownerID, DefaultMaxUploadBytes)
|
||||
}
|
||||
|
||||
func newFileHandlerTestRouterWithLimit(t *testing.T, currentUserID, ownerID uint, maxUploadBytes int64) (*gin.Engine, *gorm.DB, models.SenlinAgentProject, string) {
|
||||
func newFileHandlerTestRouterWithLimit(t *testing.T, currentUserID, ownerID uint, maxUploadBytes int64) (*gin.Engine, *gorm.DB, models.SaProject, string) {
|
||||
router, database, project, storageRoot, _ := newFileHandlerTestRouterWithService(t, currentUserID, ownerID, maxUploadBytes)
|
||||
return router, database, project, storageRoot
|
||||
}
|
||||
|
||||
func newFileHandlerTestRouterWithService(t *testing.T, currentUserID, ownerID uint, maxUploadBytes int64) (*gin.Engine, *gorm.DB, models.SenlinAgentProject, string, *Service) {
|
||||
func newFileHandlerTestRouterWithService(t *testing.T, currentUserID, ownerID uint, maxUploadBytes int64) (*gin.Engine, *gorm.DB, models.SaProject, string, *Service) {
|
||||
t.Helper()
|
||||
database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{TranslateError: true})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, models.AutoMigrate(database))
|
||||
models.DBService = database
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "other@example.com", DisplayName: "Other", PasswordHash: "hash"}).Error)
|
||||
project := models.SenlinAgentProject{OwnerID: ownerID, Name: "Files", Identifier: fmt.Sprintf("FILES-%d", ownerID)}
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "other@example.com", DisplayName: "Other", PasswordHash: "hash"}).Error)
|
||||
project := models.SaProject{OwnerID: ownerID, Name: "Files", Identifier: fmt.Sprintf("FILES-%d", ownerID)}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
storageRoot := t.TempDir()
|
||||
service := NewService(storageRoot, database)
|
||||
|
||||
@@ -193,7 +193,7 @@ func randomStorageKey() (string, error) {
|
||||
}
|
||||
|
||||
// CreateSource 只持久化 Save 产生的相对路径,不接受 handler 自行拼接本地路径。
|
||||
func (s *Service) CreateSource(ownerID uint, project *models.SenlinAgentProject, title string, stored StoredFile) (*models.SenlinAgentSource, error) {
|
||||
func (s *Service) CreateSource(ownerID uint, project *models.SaProject, title string, stored StoredFile) (*models.SaSource, error) {
|
||||
title = strings.TrimSpace(title)
|
||||
if title == "" {
|
||||
title = stored.OriginalName
|
||||
@@ -205,7 +205,7 @@ func (s *Service) CreateSource(ownerID uint, project *models.SenlinAgentProject,
|
||||
if relativePath == "" || filepath.IsAbs(relativePath) {
|
||||
return nil, ErrSourcePathRequired
|
||||
}
|
||||
source := &models.SenlinAgentSource{
|
||||
source := &models.SaSource{
|
||||
ProjectID: project.ID, ProjectIdentity: project.Identity, CreatedBy: ownerID,
|
||||
Kind: "file", Title: title, FilePath: relativePath,
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ func currentInboxUser(c *gin.Context) (uint, bool) {
|
||||
return userID, true
|
||||
}
|
||||
|
||||
func inboxItemDTO(item models.SenlinAgentInboxItem) InboxItemDTO {
|
||||
func inboxItemDTO(item models.SaInboxItem) InboxItemDTO {
|
||||
return InboxItemDTO{
|
||||
ID: item.Identity, ProjectID: item.ProjectIdentity, SourceType: item.SourceType,
|
||||
Title: item.Title, Body: item.Body, Status: item.Status,
|
||||
|
||||
@@ -40,14 +40,14 @@ type ConfirmResult struct {
|
||||
}
|
||||
|
||||
type Analyzer interface {
|
||||
Analyze(item models.SenlinAgentInboxItem, userID uint) ([]Suggestion, error)
|
||||
Analyze(item models.SaInboxItem, userID uint) ([]Suggestion, error)
|
||||
}
|
||||
|
||||
type StaticAnalyzer struct {
|
||||
Suggestions []Suggestion
|
||||
}
|
||||
|
||||
func (a StaticAnalyzer) Analyze(item models.SenlinAgentInboxItem, userID uint) ([]Suggestion, error) {
|
||||
func (a StaticAnalyzer) Analyze(item models.SaInboxItem, userID uint) ([]Suggestion, error) {
|
||||
if len(a.Suggestions) > 0 {
|
||||
return a.Suggestions, nil
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func (s *Service) database() *gorm.DB {
|
||||
}
|
||||
|
||||
// Capture 先以 owner + project identity 校验项目边界,再创建归属于当前用户的原始 Inbox 条目。
|
||||
func (s *Service) Capture(input CaptureInput) (*models.SenlinAgentInboxItem, error) {
|
||||
func (s *Service) Capture(input CaptureInput) (*models.SaInboxItem, error) {
|
||||
if strings.TrimSpace(input.ProjectIdentity) == "" || input.UserID == 0 {
|
||||
return nil, ErrProjectAndUserRequired
|
||||
}
|
||||
@@ -93,13 +93,13 @@ func (s *Service) Capture(input CaptureInput) (*models.SenlinAgentInboxItem, err
|
||||
if sourceType == "" {
|
||||
return nil, ErrSourceTypeRequired
|
||||
}
|
||||
var item models.SenlinAgentInboxItem
|
||||
var item models.SaInboxItem
|
||||
err := s.database().Transaction(func(tx *gorm.DB) error {
|
||||
var project models.SenlinAgentProject
|
||||
var project models.SaProject
|
||||
if err := tx.Where("owner_id = ? AND identity = ?", input.UserID, input.ProjectIdentity).First(&project).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
item = models.SenlinAgentInboxItem{
|
||||
item = models.SaInboxItem{
|
||||
ProjectID: project.ID, ProjectIdentity: project.Identity, CreatedBy: input.UserID,
|
||||
SourceType: sourceType, Title: strings.TrimSpace(input.Title), Body: strings.TrimSpace(input.Body), Status: "open",
|
||||
}
|
||||
@@ -138,11 +138,11 @@ func (s *Service) Analyze(itemIdentity string, userID uint) ([]Suggestion, error
|
||||
if locked.Status != "open" {
|
||||
return ErrInboxAlreadyConfirmed
|
||||
}
|
||||
if err := tx.Where("inbox_item_id = ?", locked.ID).Delete(&models.SenlinAgentInboxSuggestion{}).Error; err != nil {
|
||||
if err := tx.Where("inbox_item_id = ?", locked.ID).Delete(&models.SaInboxSuggestion{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, suggestion := range normalized {
|
||||
draft := models.SenlinAgentInboxSuggestion{
|
||||
draft := models.SaInboxSuggestion{
|
||||
InboxItemID: locked.ID, InboxItemIdentity: locked.Identity, CreatedBy: userID,
|
||||
Kind: suggestion.Kind, Title: suggestion.Title, Body: suggestion.Body,
|
||||
}
|
||||
@@ -174,14 +174,14 @@ func (s *Service) Confirm(itemIdentity string, userID uint, selectedSuggestionId
|
||||
return err
|
||||
}
|
||||
|
||||
var saved []models.SenlinAgentInboxSuggestion
|
||||
var saved []models.SaInboxSuggestion
|
||||
if err := tx.Where("inbox_item_id = ? AND created_by = ? AND identity IN ?", item.ID, userID, selected).Find(&saved).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(saved) != len(selected) {
|
||||
return ErrSuggestionNotFound
|
||||
}
|
||||
byIdentity := make(map[string]models.SenlinAgentInboxSuggestion, len(saved))
|
||||
byIdentity := make(map[string]models.SaInboxSuggestion, len(saved))
|
||||
for _, suggestion := range saved {
|
||||
byIdentity[suggestion.Identity] = suggestion
|
||||
}
|
||||
@@ -203,11 +203,11 @@ func (s *Service) Confirm(itemIdentity string, userID uint, selectedSuggestionId
|
||||
return result, err
|
||||
}
|
||||
|
||||
func findOwnedInboxItem(tx *gorm.DB, identity string, userID uint, lock bool) (*models.SenlinAgentInboxItem, error) {
|
||||
var item models.SenlinAgentInboxItem
|
||||
query := tx.Model(&models.SenlinAgentInboxItem{}).
|
||||
Joins("JOIN senlin_agent_projects ON senlin_agent_projects.id = senlin_agent_inbox_items.project_id").
|
||||
Where("senlin_agent_inbox_items.identity = ? AND senlin_agent_inbox_items.created_by = ? AND senlin_agent_projects.owner_id = ?", identity, userID, userID)
|
||||
func findOwnedInboxItem(tx *gorm.DB, identity string, userID uint, lock bool) (*models.SaInboxItem, error) {
|
||||
var item models.SaInboxItem
|
||||
query := tx.Model(&models.SaInboxItem{}).
|
||||
Joins("JOIN sa_projects ON sa_projects.id = sa_inbox_items.project_id").
|
||||
Where("sa_inbox_items.identity = ? AND sa_inbox_items.created_by = ? AND sa_projects.owner_id = ?", identity, userID, userID)
|
||||
if lock {
|
||||
query = query.Clauses(clause.Locking{Strength: "UPDATE"})
|
||||
}
|
||||
@@ -230,7 +230,7 @@ func normalizeSuggestions(suggestions []Suggestion) ([]Suggestion, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func suggestionSubject(item models.SenlinAgentInboxItem) string {
|
||||
func suggestionSubject(item models.SaInboxItem) string {
|
||||
value := strings.TrimSpace(item.Title)
|
||||
if value == "" {
|
||||
value = strings.TrimSpace(item.Body)
|
||||
@@ -266,22 +266,22 @@ func normalizeSelectedIdentities(values []string) ([]string, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func createConfirmedObject(tx *gorm.DB, item models.SenlinAgentInboxItem, suggestion models.SenlinAgentInboxSuggestion) error {
|
||||
func createConfirmedObject(tx *gorm.DB, item models.SaInboxItem, suggestion models.SaInboxSuggestion) error {
|
||||
sourceInboxItemID := item.ID
|
||||
switch suggestion.Kind {
|
||||
case "task":
|
||||
return tx.Create(&models.SenlinAgentTask{
|
||||
return tx.Create(&models.SaTask{
|
||||
ProjectID: item.ProjectID, ProjectIdentity: item.ProjectIdentity, CreatedBy: item.CreatedBy,
|
||||
SourceInboxItemID: &sourceInboxItemID, Title: suggestion.Title, Description: suggestion.Body, Status: "open",
|
||||
}).Error
|
||||
case "note":
|
||||
return tx.Create(&models.SenlinAgentNote{
|
||||
return tx.Create(&models.SaNote{
|
||||
ProjectID: item.ProjectID, ProjectIdentity: item.ProjectIdentity, CreatedBy: item.CreatedBy,
|
||||
SourceInboxItemID: &sourceInboxItemID, Title: suggestion.Title, Markdown: suggestion.Body,
|
||||
}).Error
|
||||
case "source":
|
||||
// 文本型 Inbox 资料不是上传文件,不构造或伪造任何本地路径。
|
||||
return tx.Create(&models.SenlinAgentSource{
|
||||
return tx.Create(&models.SaSource{
|
||||
ProjectID: item.ProjectID, ProjectIdentity: item.ProjectIdentity, CreatedBy: item.CreatedBy,
|
||||
SourceInboxItemID: &sourceInboxItemID, Kind: "text", Title: suggestion.Title, ContentText: suggestion.Body,
|
||||
}).Error
|
||||
|
||||
@@ -21,9 +21,9 @@ import (
|
||||
|
||||
type inboxTestFixture struct {
|
||||
database *gorm.DB
|
||||
owner models.SenlinAgentUser
|
||||
other models.SenlinAgentUser
|
||||
project models.SenlinAgentProject
|
||||
owner models.SaUser
|
||||
other models.SaUser
|
||||
project models.SaProject
|
||||
}
|
||||
|
||||
type inboxSuggestionResponse struct {
|
||||
@@ -38,7 +38,7 @@ type inboxAnalyzeResponse struct {
|
||||
}
|
||||
|
||||
func TestStaticAnalyzerBuildsReviewableDraftsFromInboxContent(t *testing.T) {
|
||||
item := models.SenlinAgentInboxItem{Title: "客户访谈", Body: "客户希望下周确认交付计划。"}
|
||||
item := models.SaInboxItem{Title: "客户访谈", Body: "客户希望下周确认交付计划。"}
|
||||
|
||||
suggestions, err := (StaticAnalyzer{}).Analyze(item, 7)
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestInboxCaptureUsesOwnedUUIDv7AndCamelCaseDTO(t *testing.T) {
|
||||
require.Equal(t, "open", payload["status"])
|
||||
requireUUIDv7(t, payload["id"].(string))
|
||||
|
||||
var item models.SenlinAgentInboxItem
|
||||
var item models.SaInboxItem
|
||||
require.NoError(t, fixture.database.Where("identity = ?", payload["id"]).First(&item).Error)
|
||||
require.Equal(t, fixture.owner.ID, item.CreatedBy)
|
||||
require.Equal(t, fixture.owner.Identity, item.CreatedByIdentity)
|
||||
@@ -87,7 +87,7 @@ func TestInboxCaptureRejectsNumericAndUnownedProjectIdentities(t *testing.T) {
|
||||
})
|
||||
require.Equal(t, http.StatusBadRequest, numeric.Code)
|
||||
|
||||
otherProject := models.SenlinAgentProject{OwnerID: fixture.other.ID, Name: "Other", Identifier: "OTHER"}
|
||||
otherProject := models.SaProject{OwnerID: fixture.other.ID, Name: "Other", Identifier: "OTHER"}
|
||||
require.NoError(t, fixture.database.Create(&otherProject).Error)
|
||||
unowned := performInboxJSON(t, router, http.MethodPost, "/api/v1/projects/"+otherProject.Identity+"/inbox", gin.H{
|
||||
"sourceType": "text", "body": "not mine",
|
||||
@@ -95,7 +95,7 @@ func TestInboxCaptureRejectsNumericAndUnownedProjectIdentities(t *testing.T) {
|
||||
require.Equal(t, http.StatusNotFound, unowned.Code)
|
||||
|
||||
var count int64
|
||||
require.NoError(t, fixture.database.Model(&models.SenlinAgentInboxItem{}).Count(&count).Error)
|
||||
require.NoError(t, fixture.database.Model(&models.SaInboxItem{}).Count(&count).Error)
|
||||
require.Zero(t, count)
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ func TestAnalyzeUsesInboxIdentityAndCreatesNoFormalObjects(t *testing.T) {
|
||||
requireUUIDv7(t, analysis.Suggestions[0].ID)
|
||||
requireFormalObjectCounts(t, fixture.database, 0, 0, 0)
|
||||
|
||||
var reloaded models.SenlinAgentInboxItem
|
||||
var reloaded models.SaInboxItem
|
||||
require.NoError(t, fixture.database.First(&reloaded, item.ID).Error)
|
||||
require.Equal(t, "open", reloaded.Status)
|
||||
}
|
||||
@@ -148,21 +148,21 @@ func TestConfirmCreatesOnlySelectedSavedSuggestionsWithInboxIdentity(t *testing.
|
||||
require.Equal(t, 3, result.CreatedCount)
|
||||
requireFormalObjectCounts(t, fixture.database, 1, 1, 1)
|
||||
|
||||
var task models.SenlinAgentTask
|
||||
var task models.SaTask
|
||||
require.NoError(t, fixture.database.First(&task).Error)
|
||||
require.Equal(t, "跟进报价", task.Title)
|
||||
require.Equal(t, fixture.project.ID, task.ProjectID)
|
||||
require.Equal(t, fixture.owner.ID, task.CreatedBy)
|
||||
requireSourceInboxIdentity(t, item, task.SourceInboxItemID, task.SourceInboxItemIdentity)
|
||||
|
||||
var note models.SenlinAgentNote
|
||||
var note models.SaNote
|
||||
require.NoError(t, fixture.database.First(¬e).Error)
|
||||
require.Equal(t, "会议纪要", note.Title)
|
||||
require.Equal(t, fixture.project.ID, note.ProjectID)
|
||||
require.Equal(t, fixture.owner.ID, note.CreatedBy)
|
||||
requireSourceInboxIdentity(t, item, note.SourceInboxItemID, note.SourceInboxItemIdentity)
|
||||
|
||||
var source models.SenlinAgentSource
|
||||
var source models.SaSource
|
||||
require.NoError(t, fixture.database.First(&source).Error)
|
||||
require.Equal(t, "背景资料", source.Title)
|
||||
require.Equal(t, "text", source.Kind)
|
||||
@@ -173,9 +173,9 @@ func TestConfirmCreatesOnlySelectedSavedSuggestionsWithInboxIdentity(t *testing.
|
||||
requireSourceInboxIdentity(t, item, source.SourceInboxItemID, source.SourceInboxItemIdentity)
|
||||
|
||||
var skipped int64
|
||||
require.NoError(t, fixture.database.Model(&models.SenlinAgentTask{}).Where("title = ?", "不创建的任务").Count(&skipped).Error)
|
||||
require.NoError(t, fixture.database.Model(&models.SaTask{}).Where("title = ?", "不创建的任务").Count(&skipped).Error)
|
||||
require.Zero(t, skipped)
|
||||
var reloaded models.SenlinAgentInboxItem
|
||||
var reloaded models.SaInboxItem
|
||||
require.NoError(t, fixture.database.First(&reloaded, item.ID).Error)
|
||||
require.Equal(t, "processed", reloaded.Status)
|
||||
}
|
||||
@@ -234,7 +234,7 @@ func TestConfirmIsTransactionalWhenASelectedWriteFails(t *testing.T) {
|
||||
|
||||
const callbackName = "test:fail_inbox_source_create"
|
||||
require.NoError(t, fixture.database.Callback().Create().Before("gorm:create").Register(callbackName, func(tx *gorm.DB) {
|
||||
if tx.Statement.Schema != nil && tx.Statement.Schema.Name == "SenlinAgentSource" {
|
||||
if tx.Statement.Schema != nil && tx.Statement.Schema.Name == "SaSource" {
|
||||
tx.AddError(errors.New("simulated source write failure"))
|
||||
}
|
||||
}))
|
||||
@@ -246,7 +246,7 @@ func TestConfirmIsTransactionalWhenASelectedWriteFails(t *testing.T) {
|
||||
|
||||
require.Equal(t, http.StatusInternalServerError, response.Code)
|
||||
requireFormalObjectCounts(t, fixture.database, 0, 0, 0)
|
||||
var reloaded models.SenlinAgentInboxItem
|
||||
var reloaded models.SaInboxItem
|
||||
require.NoError(t, fixture.database.First(&reloaded, item.ID).Error)
|
||||
require.Equal(t, "open", reloaded.Status)
|
||||
}
|
||||
@@ -268,7 +268,7 @@ func TestRepeatedConfirmReturnsPersistedResultAfterRelatedObjectsChange(t *testi
|
||||
requireFormalObjectCounts(t, fixture.database, 1, 1, 0)
|
||||
|
||||
sourceInboxItemID := item.ID
|
||||
extraSource := models.SenlinAgentSource{
|
||||
extraSource := models.SaSource{
|
||||
ProjectID: item.ProjectID, CreatedBy: fixture.owner.ID, SourceInboxItemID: &sourceInboxItemID,
|
||||
Kind: "text", Title: "后续关联资料", ContentText: "不得改变历史确认结果",
|
||||
}
|
||||
@@ -277,7 +277,7 @@ func TestRepeatedConfirmReturnsPersistedResultAfterRelatedObjectsChange(t *testi
|
||||
require.Equal(t, http.StatusOK, second.Code, second.Body.String())
|
||||
requireConfirmCreatedCount(t, second, 2)
|
||||
|
||||
var createdTask models.SenlinAgentTask
|
||||
var createdTask models.SaTask
|
||||
require.NoError(t, fixture.database.Where("source_inbox_item_id = ?", item.ID).First(&createdTask).Error)
|
||||
require.NoError(t, fixture.database.Delete(&createdTask).Error)
|
||||
require.NoError(t, fixture.database.Delete(&extraSource).Error)
|
||||
@@ -297,9 +297,9 @@ func (fixture inboxTestFixture) router(userID uint, analyzer Analyzer) http.Hand
|
||||
)
|
||||
}
|
||||
|
||||
func (fixture inboxTestFixture) createInbox(t *testing.T, userID, projectID uint) models.SenlinAgentInboxItem {
|
||||
func (fixture inboxTestFixture) createInbox(t *testing.T, userID, projectID uint) models.SaInboxItem {
|
||||
t.Helper()
|
||||
item := models.SenlinAgentInboxItem{
|
||||
item := models.SaInboxItem{
|
||||
ProjectID: projectID, CreatedBy: userID, SourceType: "text", Title: "待整理", Body: "需要整理的原始内容", Status: "open",
|
||||
}
|
||||
require.NoError(t, fixture.database.Create(&item).Error)
|
||||
@@ -309,11 +309,11 @@ func (fixture inboxTestFixture) createInbox(t *testing.T, userID, projectID uint
|
||||
func newInboxTestFixture(t *testing.T) inboxTestFixture {
|
||||
t.Helper()
|
||||
database := newTestDB(t)
|
||||
owner := models.SenlinAgentUser{Email: t.Name() + "-owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}
|
||||
other := models.SenlinAgentUser{Email: t.Name() + "-other@example.com", DisplayName: "Other", PasswordHash: "hash"}
|
||||
owner := models.SaUser{Email: t.Name() + "-owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}
|
||||
other := models.SaUser{Email: t.Name() + "-other@example.com", DisplayName: "Other", PasswordHash: "hash"}
|
||||
require.NoError(t, database.Create(&owner).Error)
|
||||
require.NoError(t, database.Create(&other).Error)
|
||||
project := models.SenlinAgentProject{OwnerID: owner.ID, Name: "Inbox Project", Identifier: "INBOX"}
|
||||
project := models.SaProject{OwnerID: owner.ID, Name: "Inbox Project", Identifier: "INBOX"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
return inboxTestFixture{database: database, owner: owner, other: other, project: project}
|
||||
}
|
||||
@@ -348,9 +348,9 @@ func requireFormalObjectCounts(t *testing.T, database *gorm.DB, tasks, notes, so
|
||||
model any
|
||||
want int64
|
||||
}{
|
||||
{model: &models.SenlinAgentTask{}, want: tasks},
|
||||
{model: &models.SenlinAgentNote{}, want: notes},
|
||||
{model: &models.SenlinAgentSource{}, want: sources},
|
||||
{model: &models.SaTask{}, want: tasks},
|
||||
{model: &models.SaNote{}, want: notes},
|
||||
{model: &models.SaSource{}, want: sources},
|
||||
} {
|
||||
var count int64
|
||||
require.NoError(t, database.Model(check.model).Count(&count).Error)
|
||||
@@ -365,7 +365,7 @@ func requireConfirmCreatedCount(t *testing.T, response *httptest.ResponseRecorde
|
||||
require.Equal(t, want, result.CreatedCount)
|
||||
}
|
||||
|
||||
func requireSourceInboxIdentity(t *testing.T, item models.SenlinAgentInboxItem, sourceID *uint, sourceIdentity *string) {
|
||||
func requireSourceInboxIdentity(t *testing.T, item models.SaInboxItem, sourceID *uint, sourceIdentity *string) {
|
||||
t.Helper()
|
||||
require.NotNil(t, sourceID)
|
||||
require.Equal(t, item.ID, *sourceID)
|
||||
|
||||
@@ -14,11 +14,11 @@ func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
func (s *Service) CreateNote(projectID uint, userID uint, title string, markdown string) (*models.SenlinAgentNote, error) {
|
||||
func (s *Service) CreateNote(projectID uint, userID uint, title string, markdown string) (*models.SaNote, error) {
|
||||
title = strings.TrimSpace(title)
|
||||
if title == "" {
|
||||
return nil, errors.New("note title is required")
|
||||
}
|
||||
note := &models.SenlinAgentNote{ProjectID: projectID, CreatedBy: userID, Title: title, Markdown: markdown}
|
||||
note := &models.SaNote{ProjectID: projectID, CreatedBy: userID, Title: title, Markdown: markdown}
|
||||
return note, models.DBService.Create(note).Error
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ func writeCronError(c *gin.Context, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func cronPlanDTO(plan models.SenlinAgentCronPlan) CronPlanDTO {
|
||||
func cronPlanDTO(plan models.SaCronPlan) CronPlanDTO {
|
||||
return CronPlanDTO{
|
||||
ID: plan.Identity, ProjectID: plan.ProjectIdentity, Title: plan.Title, Schedule: plan.Schedule,
|
||||
NextRunAt: projectUTCOptionalTime(plan.NextRunAt), Enabled: plan.Enabled, LastResult: plan.LastResult,
|
||||
|
||||
@@ -144,7 +144,7 @@ type WorkspaceCronPlanDTO struct {
|
||||
Owner string `json:"owner"`
|
||||
}
|
||||
|
||||
func projectDTO(project models.SenlinAgentProject) ProjectDTO {
|
||||
func projectDTO(project models.SaProject) ProjectDTO {
|
||||
return ProjectDTO{
|
||||
ID: project.Identity,
|
||||
Name: project.Name,
|
||||
|
||||
@@ -32,7 +32,7 @@ func TestCreateProjectHandlerPersistsMetadata(t *testing.T) {
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusCreated, rec.Code)
|
||||
var project models.SenlinAgentProject
|
||||
var project models.SaProject
|
||||
require.NoError(t, models.DBService.Where("owner_id = ? AND identifier = ?", 1, "EXP").First(&project).Error)
|
||||
require.Equal(t, "Explore", project.Name)
|
||||
require.Equal(t, "compass", project.Icon)
|
||||
@@ -138,7 +138,7 @@ func TestUpdateProject(t *testing.T) {
|
||||
require.Equal(t, "tree", payload["icon"])
|
||||
require.Equal(t, "#0FC6C2", payload["background"])
|
||||
require.Equal(t, "更新后的项目说明", payload["description"])
|
||||
var updated models.SenlinAgentProject
|
||||
var updated models.SaProject
|
||||
require.NoError(t, models.DBService.First(&updated, project.ID).Error)
|
||||
require.Equal(t, "Alpha Next", updated.Name)
|
||||
require.Equal(t, "ALPHA-NEXT", updated.Identifier)
|
||||
@@ -178,7 +178,7 @@ func TestUpdateProject(t *testing.T) {
|
||||
require.Empty(t, payload.Icon)
|
||||
require.Empty(t, payload.Background)
|
||||
require.Empty(t, payload.Description)
|
||||
var updated models.SenlinAgentProject
|
||||
var updated models.SaProject
|
||||
require.NoError(t, models.DBService.First(&updated, project.ID).Error)
|
||||
require.Equal(t, originalName, updated.Name)
|
||||
require.Equal(t, originalIdentifier, updated.Identifier)
|
||||
@@ -228,10 +228,10 @@ func TestUpdateProject(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func newProjectsHandlerTestRouter(t *testing.T) (*gin.Engine, *models.SenlinAgentProject, string) {
|
||||
func newProjectsHandlerTestRouter(t *testing.T) (*gin.Engine, *models.SaProject, string) {
|
||||
t.Helper()
|
||||
newTestDB(t)
|
||||
require.NoError(t, models.DBService.Create(&models.SenlinAgentUser{Email: "david@example.com", DisplayName: "David", PasswordHash: "hash"}).Error)
|
||||
require.NoError(t, models.DBService.Create(&models.SaUser{Email: "david@example.com", DisplayName: "David", PasswordHash: "hash"}).Error)
|
||||
service := NewService()
|
||||
project, err := service.CreateProject(1, "Alpha", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -16,7 +16,7 @@ var (
|
||||
ErrCronScheduleRequired = errors.New("cron schedule is required")
|
||||
)
|
||||
|
||||
func (s *Service) CreateCronPlan(ownerID uint, projectID uint, input CreateCronPlanInput) (*models.SenlinAgentCronPlan, error) {
|
||||
func (s *Service) CreateCronPlan(ownerID uint, projectID uint, input CreateCronPlanInput) (*models.SaCronPlan, error) {
|
||||
if err := ensureProjectOwner(ownerID, projectID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -28,34 +28,34 @@ func (s *Service) CreateCronPlan(ownerID uint, projectID uint, input CreateCronP
|
||||
if schedule == "" {
|
||||
return nil, ErrCronScheduleRequired
|
||||
}
|
||||
plan := &models.SenlinAgentCronPlan{
|
||||
plan := &models.SaCronPlan{
|
||||
ProjectID: projectID, CreatedBy: ownerID, Title: title, Schedule: schedule,
|
||||
Enabled: input.Enabled, NextRunAt: input.NextRunAt,
|
||||
}
|
||||
return plan, models.DBService.Create(plan).Error
|
||||
}
|
||||
|
||||
func (s *Service) CreateProjectTag(ownerID uint, projectID uint, name string) (*models.SenlinAgentTag, error) {
|
||||
func (s *Service) CreateProjectTag(ownerID uint, projectID uint, name string) (*models.SaTag, error) {
|
||||
if err := ensureProjectOwner(ownerID, projectID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.CreateTag(projectID, name)
|
||||
}
|
||||
|
||||
func (s *Service) CreateTag(projectID uint, name string) (*models.SenlinAgentTag, error) {
|
||||
func (s *Service) CreateTag(projectID uint, name string) (*models.SaTag, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return nil, ErrTagNameRequired
|
||||
}
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 5; attempt++ {
|
||||
candidate := models.SenlinAgentTag{ProjectID: projectID, Name: name}
|
||||
candidate := models.SaTag{ProjectID: projectID, Name: name}
|
||||
lastErr = models.DBService.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "project_id"}, {Name: "name"}},
|
||||
DoNothing: true,
|
||||
}).Create(&candidate).Error
|
||||
if lastErr == nil {
|
||||
var stored models.SenlinAgentTag
|
||||
var stored models.SaTag
|
||||
lastErr = models.DBService.Where("project_id = ? AND name = ?", projectID, name).First(&stored).Error
|
||||
if lastErr == nil {
|
||||
return &stored, nil
|
||||
@@ -83,22 +83,22 @@ func retryableTagWrite(err error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Service) ListProjectTags(ownerID uint, projectID uint) ([]models.SenlinAgentTag, error) {
|
||||
func (s *Service) ListProjectTags(ownerID uint, projectID uint) ([]models.SaTag, error) {
|
||||
if err := ensureProjectOwner(ownerID, projectID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.ListTags(projectID)
|
||||
}
|
||||
|
||||
func (s *Service) ListTags(projectID uint) ([]models.SenlinAgentTag, error) {
|
||||
var tags []models.SenlinAgentTag
|
||||
func (s *Service) ListTags(projectID uint) ([]models.SaTag, error) {
|
||||
var tags []models.SaTag
|
||||
err := models.DBService.Where("project_id = ?", projectID).Order("name asc").Find(&tags).Error
|
||||
return tags, err
|
||||
}
|
||||
|
||||
func ensureProjectOwner(ownerID uint, projectID uint) error {
|
||||
var count int64
|
||||
if err := models.DBService.Model(&models.SenlinAgentProject{}).Where("id = ? AND owner_id = ?", projectID, ownerID).Count(&count).Error; err != nil {
|
||||
if err := models.DBService.Model(&models.SaProject{}).Where("id = ? AND owner_id = ?", projectID, ownerID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
|
||||
// FindOwnedProject 同时按 owner_id 与公开 identity 查询,防止仅凭可猜测标识越权访问项目。
|
||||
// 内部自增 ID 只在通过所有权校验后供关联查询使用,不进入 API 契约。
|
||||
func FindOwnedProject(userID uint, identity string) (*models.SenlinAgentProject, error) {
|
||||
var project models.SenlinAgentProject
|
||||
func FindOwnedProject(userID uint, identity string) (*models.SaProject, error) {
|
||||
var project models.SaProject
|
||||
result := models.DBService.Where("owner_id = ? AND identity = ?", userID, identity).Limit(1).Find(&project)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
|
||||
@@ -14,11 +14,11 @@ func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
func (s *Service) CreateProject(ownerID uint, name string, description string) (*models.SenlinAgentProject, error) {
|
||||
func (s *Service) CreateProject(ownerID uint, name string, description string) (*models.SaProject, error) {
|
||||
return s.CreateProjectWithInput(ownerID, CreateProjectRequest{Name: name, Description: description})
|
||||
}
|
||||
|
||||
func (s *Service) CreateProjectWithInput(ownerID uint, input CreateProjectRequest) (*models.SenlinAgentProject, error) {
|
||||
func (s *Service) CreateProjectWithInput(ownerID uint, input CreateProjectRequest) (*models.SaProject, error) {
|
||||
name := strings.TrimSpace(input.Name)
|
||||
if name == "" {
|
||||
return nil, ErrProjectNameRequired
|
||||
@@ -27,7 +27,7 @@ func (s *Service) CreateProjectWithInput(ownerID uint, input CreateProjectReques
|
||||
if identifier == "" {
|
||||
identifier = projectInitials(name)
|
||||
}
|
||||
project := &models.SenlinAgentProject{
|
||||
project := &models.SaProject{
|
||||
OwnerID: ownerID, Name: name, Identifier: identifier,
|
||||
Icon: strings.TrimSpace(input.Icon), Background: strings.TrimSpace(input.Background),
|
||||
Description: strings.TrimSpace(input.Description),
|
||||
@@ -40,7 +40,7 @@ func (s *Service) CreateProjectWithInput(ownerID uint, input CreateProjectReques
|
||||
|
||||
// ListProjects 只返回当前所有者的公开 DTO,不让 handler 接触或序列化数据库模型。
|
||||
func (s *Service) ListProjects(ownerID uint) ([]ProjectDTO, error) {
|
||||
var projects []models.SenlinAgentProject
|
||||
var projects []models.SaProject
|
||||
if err := models.DBService.Where("owner_id = ?", ownerID).Order("updated_at desc").Find(&projects).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -37,21 +37,21 @@ func TestFindOwnedProjectScopesIdentityByOwner(t *testing.T) {
|
||||
|
||||
func TestProjectIdentifierDatabaseConstraintIsScopedByOwner(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
first := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "SHARED"}
|
||||
first := models.SaProject{OwnerID: 1, Name: "Alpha", Identifier: "SHARED"}
|
||||
require.NoError(t, database.Create(&first).Error)
|
||||
|
||||
duplicate := models.SenlinAgentProject{OwnerID: 1, Name: "Duplicate", Identifier: "SHARED"}
|
||||
duplicate := models.SaProject{OwnerID: 1, Name: "Duplicate", Identifier: "SHARED"}
|
||||
require.ErrorIs(t, database.Create(&duplicate).Error, gorm.ErrDuplicatedKey)
|
||||
|
||||
otherOwner := models.SenlinAgentProject{OwnerID: 2, Name: "Allowed", Identifier: "SHARED"}
|
||||
otherOwner := models.SaProject{OwnerID: 2, Name: "Allowed", Identifier: "SHARED"}
|
||||
require.NoError(t, database.Create(&otherOwner).Error)
|
||||
}
|
||||
|
||||
func TestProjectIdentifierDatabaseErrorMapsToConflict(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
first := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "SHARED"}
|
||||
first := models.SaProject{OwnerID: 1, Name: "Alpha", Identifier: "SHARED"}
|
||||
require.NoError(t, database.Create(&first).Error)
|
||||
duplicate := models.SenlinAgentProject{OwnerID: 1, Name: "Duplicate", Identifier: "SHARED"}
|
||||
duplicate := models.SaProject{OwnerID: 1, Name: "Duplicate", Identifier: "SHARED"}
|
||||
databaseErr := database.Create(&duplicate).Error
|
||||
require.ErrorIs(t, databaseErr, gorm.ErrDuplicatedKey)
|
||||
|
||||
@@ -113,26 +113,26 @@ func TestCreateProjectPersistsWorkspaceMetadata(t *testing.T) {
|
||||
func TestWorkspaceMatchesFrontendContract(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
service := NewService()
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "david@example.com", DisplayName: "David", PasswordHash: "hash"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "david@example.com", DisplayName: "David", PasswordHash: "hash"}).Error)
|
||||
project, err := service.CreateProject(1, "Project A1", "Client strategy workspace")
|
||||
require.NoError(t, err)
|
||||
other, err := service.CreateProject(1, "Project A2", "")
|
||||
require.NoError(t, err)
|
||||
nextRun := time.Date(2026, 7, 20, 9, 0, 0, 0, time.UTC)
|
||||
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTag{ProjectID: project.ID, Name: "UI"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTag{ProjectID: project.ID, Name: "knowledge"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentInboxItem{ProjectID: project.ID, CreatedBy: 1, SourceType: "Manual", Title: "Collect notes", Body: "Turn pasted research into tasks.", Status: "open"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentInboxItem{ProjectID: other.ID, CreatedBy: 1, SourceType: "Manual", Title: "Other notes", Body: "Hidden", Status: "open"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: 1, Title: "Confirm IA", Description: "Review channel layout.", Status: "open", DueAt: &nextRun}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: 1, Title: "Archive notes", Description: "Move outdated notes.", Status: "done"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentAISession{ProjectID: project.ID, CreatedBy: 1, Title: "UI prototype critique", Context: "Discuss channel behavior."}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentNote{ProjectID: project.ID, CreatedBy: 1, Title: "Workbench principles", Markdown: "# Notes"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentSource{ProjectID: project.ID, CreatedBy: 1, Kind: "link", Title: "Reference board", URL: "https://example.com"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentProjectChannel{ProjectID: project.ID, Title: "Roadmap Board", Icon: "link", URL: "https://example.com/roadmap-a1", SortOrder: 7}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentProjectChannel{ProjectID: other.ID, Title: "Other Board", Icon: "link", URL: "https://example.com/other", SortOrder: 7}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentCronPlan{ProjectID: project.ID, CreatedBy: 1, Title: "Weekly inbox review reminder", Schedule: "0 9 * * 1", NextRunAt: &nextRun, Enabled: true, LastResult: "Not run yet"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentCronPlan{ProjectID: other.ID, CreatedBy: 1, Title: "Other cron", Schedule: "0 9 * * 2", Enabled: true}).Error)
|
||||
require.NoError(t, database.Create(&models.SaTag{ProjectID: project.ID, Name: "UI"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaTag{ProjectID: project.ID, Name: "knowledge"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaInboxItem{ProjectID: project.ID, CreatedBy: 1, SourceType: "Manual", Title: "Collect notes", Body: "Turn pasted research into tasks.", Status: "open"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaInboxItem{ProjectID: other.ID, CreatedBy: 1, SourceType: "Manual", Title: "Other notes", Body: "Hidden", Status: "open"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaTask{ProjectID: project.ID, CreatedBy: 1, Title: "Confirm IA", Description: "Review channel layout.", Status: "open", DueAt: &nextRun}).Error)
|
||||
require.NoError(t, database.Create(&models.SaTask{ProjectID: project.ID, CreatedBy: 1, Title: "Archive notes", Description: "Move outdated notes.", Status: "done"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaAISession{ProjectID: project.ID, CreatedBy: 1, Title: "UI prototype critique", Context: "Discuss channel behavior."}).Error)
|
||||
require.NoError(t, database.Create(&models.SaNote{ProjectID: project.ID, CreatedBy: 1, Title: "Workbench principles", Markdown: "# Notes"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaSource{ProjectID: project.ID, CreatedBy: 1, Kind: "link", Title: "Reference board", URL: "https://example.com"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaProjectChannel{ProjectID: project.ID, Title: "Roadmap Board", Icon: "link", URL: "https://example.com/roadmap-a1", SortOrder: 7}).Error)
|
||||
require.NoError(t, database.Create(&models.SaProjectChannel{ProjectID: other.ID, Title: "Other Board", Icon: "link", URL: "https://example.com/other", SortOrder: 7}).Error)
|
||||
require.NoError(t, database.Create(&models.SaCronPlan{ProjectID: project.ID, CreatedBy: 1, Title: "Weekly inbox review reminder", Schedule: "0 9 * * 1", NextRunAt: &nextRun, Enabled: true, LastResult: "Not run yet"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaCronPlan{ProjectID: other.ID, CreatedBy: 1, Title: "Other cron", Schedule: "0 9 * * 2", Enabled: true}).Error)
|
||||
|
||||
workspace, err := service.Workspace(1, project.Identity)
|
||||
|
||||
@@ -221,7 +221,7 @@ func TestCreateProjectTagConcurrentlyReturnsOneProjectScopedTag(t *testing.T) {
|
||||
release := make(chan struct{})
|
||||
var synchronizedQueries atomic.Int32
|
||||
require.NoError(t, database.Callback().Create().Before("gorm:create").Register("test:synchronize_tag_inserts", func(tx *gorm.DB) {
|
||||
if tx.Statement.Schema == nil || tx.Statement.Schema.Table != (models.SenlinAgentTag{}).TableName() {
|
||||
if tx.Statement.Schema == nil || tx.Statement.Schema.Table != (models.SaTag{}).TableName() {
|
||||
return
|
||||
}
|
||||
if synchronizedQueries.Add(1) <= 2 {
|
||||
@@ -231,7 +231,7 @@ func TestCreateProjectTagConcurrentlyReturnsOneProjectScopedTag(t *testing.T) {
|
||||
}))
|
||||
|
||||
type result struct {
|
||||
tag *models.SenlinAgentTag
|
||||
tag *models.SaTag
|
||||
err error
|
||||
}
|
||||
results := make(chan result, 2)
|
||||
@@ -258,14 +258,14 @@ func TestCreateProjectTagConcurrentlyReturnsOneProjectScopedTag(t *testing.T) {
|
||||
}
|
||||
require.Len(t, identities, 1)
|
||||
var count int64
|
||||
require.NoError(t, database.Model(&models.SenlinAgentTag{}).Where("project_id = ? AND name = ?", project.ID, "UI").Count(&count).Error)
|
||||
require.NoError(t, database.Model(&models.SaTag{}).Where("project_id = ? AND name = ?", project.ID, "UI").Count(&count).Error)
|
||||
require.Equal(t, int64(1), count)
|
||||
}
|
||||
|
||||
func TestCreateCronPlanAddsPlanToOwnedProject(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
service := NewService()
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "david@example.com", DisplayName: "David", PasswordHash: "hash"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "david@example.com", DisplayName: "David", PasswordHash: "hash"}).Error)
|
||||
project, err := service.CreateProject(1, "Alpha", "")
|
||||
require.NoError(t, err)
|
||||
nextRun := time.Date(2026, 7, 22, 8, 0, 0, 0, time.UTC)
|
||||
|
||||
@@ -75,7 +75,7 @@ func writeTagError(c *gin.Context, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func ownedProjectFromRequest(c *gin.Context) (uint, *models.SenlinAgentProject, bool) {
|
||||
func ownedProjectFromRequest(c *gin.Context) (uint, *models.SaProject, bool) {
|
||||
userID, ok := auth.CurrentUserID(c)
|
||||
if !ok {
|
||||
httpx.Error(c, http.StatusUnauthorized, "unauthorized", "未登录或登录已失效")
|
||||
|
||||
@@ -81,32 +81,32 @@ func (s *Service) Workspace(ownerID uint, projectIdentity string) (WorkspaceDTO,
|
||||
|
||||
func (s *Service) workspaceCounts(projectID uint) (workspaceCounts, error) {
|
||||
var counts workspaceCounts
|
||||
if err := models.DBService.Model(&models.SenlinAgentInboxItem{}).Where("project_id = ? AND status = ?", projectID, "open").Count(&counts.inbox).Error; err != nil {
|
||||
if err := models.DBService.Model(&models.SaInboxItem{}).Where("project_id = ? AND status = ?", projectID, "open").Count(&counts.inbox).Error; err != nil {
|
||||
return counts, err
|
||||
}
|
||||
if err := models.DBService.Model(&models.SenlinAgentTask{}).Where("project_id = ? AND status <> ?", projectID, "done").Count(&counts.tasks).Error; err != nil {
|
||||
if err := models.DBService.Model(&models.SaTask{}).Where("project_id = ? AND status <> ?", projectID, "done").Count(&counts.tasks).Error; err != nil {
|
||||
return counts, err
|
||||
}
|
||||
if err := models.DBService.Model(&models.SenlinAgentAISession{}).Where("project_id = ?", projectID).Count(&counts.aiSessions).Error; err != nil {
|
||||
if err := models.DBService.Model(&models.SaAISession{}).Where("project_id = ?", projectID).Count(&counts.aiSessions).Error; err != nil {
|
||||
return counts, err
|
||||
}
|
||||
var noteCount int64
|
||||
if err := models.DBService.Model(&models.SenlinAgentNote{}).Where("project_id = ?", projectID).Count(¬eCount).Error; err != nil {
|
||||
if err := models.DBService.Model(&models.SaNote{}).Where("project_id = ?", projectID).Count(¬eCount).Error; err != nil {
|
||||
return counts, err
|
||||
}
|
||||
var sourceCount int64
|
||||
if err := models.DBService.Model(&models.SenlinAgentSource{}).Where("project_id = ?", projectID).Count(&sourceCount).Error; err != nil {
|
||||
if err := models.DBService.Model(&models.SaSource{}).Where("project_id = ?", projectID).Count(&sourceCount).Error; err != nil {
|
||||
return counts, err
|
||||
}
|
||||
counts.notesSources = noteCount + sourceCount
|
||||
if err := models.DBService.Model(&models.SenlinAgentCronPlan{}).Where("project_id = ?", projectID).Count(&counts.cronPlans).Error; err != nil {
|
||||
if err := models.DBService.Model(&models.SaCronPlan{}).Where("project_id = ?", projectID).Count(&counts.cronPlans).Error; err != nil {
|
||||
return counts, err
|
||||
}
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func (s *Service) workspaceTags(projectID uint) ([]WorkspaceTagDTO, error) {
|
||||
var tags []models.SenlinAgentTag
|
||||
var tags []models.SaTag
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("name asc").Find(&tags).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -127,7 +127,7 @@ func (s *Service) workspaceChannels(projectID uint, projectIdentity string, coun
|
||||
{ID: projectIdentity + ":notes", ProjectID: projectIdentity, Type: "notes_sources", Title: "Notes & Sources", Icon: "file", Count: counts.notesSources, URL: "", SortOrder: 5},
|
||||
{ID: projectIdentity + ":cron", ProjectID: projectIdentity, Type: "cron", Title: "Cron Plans", Icon: "clock", Count: counts.cronPlans, URL: "", SortOrder: 6},
|
||||
}
|
||||
var customChannels []models.SenlinAgentProjectChannel
|
||||
var customChannels []models.SaProjectChannel
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("sort_order asc, id asc").Find(&customChannels).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -141,7 +141,7 @@ func (s *Service) workspaceChannels(projectID uint, projectIdentity string, coun
|
||||
}
|
||||
|
||||
func (s *Service) workspaceInbox(projectID uint, projectIdentity string) ([]WorkspaceInboxDTO, error) {
|
||||
var records []models.SenlinAgentInboxItem
|
||||
var records []models.SaInboxItem
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("updated_at desc, id desc").Limit(50).Find(&records).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -156,7 +156,7 @@ func (s *Service) workspaceInbox(projectID uint, projectIdentity string) ([]Work
|
||||
}
|
||||
|
||||
func (s *Service) workspaceTasks(projectID uint, projectIdentity string) ([]WorkspaceTaskDTO, error) {
|
||||
var tasks []models.SenlinAgentTask
|
||||
var tasks []models.SaTask
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("CASE WHEN status = 'done' THEN 1 ELSE 0 END asc").Order("sort_order asc, updated_at desc, id desc").Limit(50).Find(&tasks).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -185,7 +185,7 @@ func (s *Service) workspaceTasks(projectID uint, projectIdentity string) ([]Work
|
||||
}
|
||||
|
||||
// workspaceTaskTags 同时限定 tag 主键和项目主键,避免损坏数据把其他项目标签带入任务响应。
|
||||
func workspaceTaskTags(projectID uint, tasks []models.SenlinAgentTask) (map[uint]WorkspaceTagDTO, error) {
|
||||
func workspaceTaskTags(projectID uint, tasks []models.SaTask) (map[uint]WorkspaceTagDTO, error) {
|
||||
tagIDs := make([]uint, 0, len(tasks))
|
||||
for _, task := range tasks {
|
||||
if task.TagID != nil {
|
||||
@@ -196,7 +196,7 @@ func workspaceTaskTags(projectID uint, tasks []models.SenlinAgentTask) (map[uint
|
||||
if len(tagIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
var tags []models.SenlinAgentTag
|
||||
var tags []models.SaTag
|
||||
if err := models.DBService.Where("project_id = ? AND id IN ?", projectID, tagIDs).Find(&tags).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -215,7 +215,7 @@ func workspaceTaskTags(projectID uint, tasks []models.SenlinAgentTask) (map[uint
|
||||
}
|
||||
|
||||
func (s *Service) workspaceAISessions(projectID uint, projectIdentity string) ([]WorkspaceAISessionDTO, error) {
|
||||
var sessions []models.SenlinAgentAISession
|
||||
var sessions []models.SaAISession
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("updated_at desc, id desc").Limit(50).Find(&sessions).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -230,7 +230,7 @@ func (s *Service) workspaceAISessions(projectID uint, projectIdentity string) ([
|
||||
}
|
||||
|
||||
func (s *Service) workspaceNotesSources(projectID uint, projectIdentity string) ([]WorkspaceNoteSourceDTO, error) {
|
||||
var notes []models.SenlinAgentNote
|
||||
var notes []models.SaNote
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("updated_at desc, id desc").Limit(50).Find(¬es).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -242,7 +242,7 @@ func (s *Service) workspaceNotesSources(projectID uint, projectIdentity string)
|
||||
})
|
||||
}
|
||||
|
||||
var sources []models.SenlinAgentSource
|
||||
var sources []models.SaSource
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("updated_at desc, id desc").Limit(50).Find(&sources).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -256,7 +256,7 @@ func (s *Service) workspaceNotesSources(projectID uint, projectIdentity string)
|
||||
}
|
||||
|
||||
func (s *Service) workspaceCronPlans(projectID uint, projectIdentity string) ([]WorkspaceCronPlanDTO, error) {
|
||||
var plans []models.SenlinAgentCronPlan
|
||||
var plans []models.SaCronPlan
|
||||
if err := models.DBService.Where("project_id = ?", projectID).Order("enabled desc, updated_at desc, id desc").Limit(50).Find(&plans).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -283,7 +283,7 @@ func utcOptionalTime(value *time.Time) *time.Time {
|
||||
return &utc
|
||||
}
|
||||
|
||||
func sourceDisplayName(source models.SenlinAgentSource) string {
|
||||
func sourceDisplayName(source models.SaSource) string {
|
||||
switch source.Kind {
|
||||
case "file":
|
||||
return "Attachment"
|
||||
@@ -299,7 +299,7 @@ func userDisplayName(assigneeID *uint, createdBy uint) (string, error) {
|
||||
if assigneeID != nil && *assigneeID != 0 {
|
||||
userID = *assigneeID
|
||||
}
|
||||
var user models.SenlinAgentUser
|
||||
var user models.SaUser
|
||||
if err := models.DBService.Select("display_name").Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "未知用户", nil
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
func TestWorkspaceUsesIdentitiesUTCTimesAndProjectScopedTags(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
service := NewService()
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "david@example.com", DisplayName: "David", PasswordHash: "hash"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "david@example.com", DisplayName: "David", PasswordHash: "hash"}).Error)
|
||||
project, err := service.CreateProjectWithInput(1, CreateProjectRequest{
|
||||
Name: "Project A1", Identifier: "A1", Icon: "folder", Background: "#165DFF", Description: "Client strategy workspace",
|
||||
})
|
||||
@@ -21,8 +21,8 @@ func TestWorkspaceUsesIdentitiesUTCTimesAndProjectScopedTags(t *testing.T) {
|
||||
other, err := service.CreateProject(1, "Project A2", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
projectTag := models.SenlinAgentTag{ProjectID: project.ID, Name: "UI"}
|
||||
otherTag := models.SenlinAgentTag{ProjectID: other.ID, Name: "UI"}
|
||||
projectTag := models.SaTag{ProjectID: project.ID, Name: "UI"}
|
||||
otherTag := models.SaTag{ProjectID: other.ID, Name: "UI"}
|
||||
require.NoError(t, database.Create(&projectTag).Error)
|
||||
require.NoError(t, database.Create(&otherTag).Error)
|
||||
|
||||
@@ -30,13 +30,13 @@ func TestWorkspaceUsesIdentitiesUTCTimesAndProjectScopedTags(t *testing.T) {
|
||||
createdAt := time.Date(2026, 7, 20, 10, 30, 0, 0, localZone)
|
||||
updatedAt := createdAt.Add(90 * time.Minute)
|
||||
nextRun := time.Date(2026, 7, 21, 9, 0, 0, 0, localZone)
|
||||
inbox := models.SenlinAgentInboxItem{ProjectID: project.ID, CreatedBy: 1, SourceType: "Manual", Title: "Collect notes", Body: "Turn research into tasks.", Status: "open", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
task := models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: 1, TagID: &projectTag.ID, Title: "Confirm IA", Description: "Review channel layout.", Status: "done", DueAt: &nextRun, CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
session := models.SenlinAgentAISession{ProjectID: project.ID, CreatedBy: 1, Title: "UI critique", Context: "Discuss channel behavior.", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
note := models.SenlinAgentNote{ProjectID: project.ID, CreatedBy: 1, Title: "Workbench principles", Markdown: "# Notes", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
source := models.SenlinAgentSource{ProjectID: project.ID, CreatedBy: 1, Kind: "link", Title: "Reference board", URL: "https://example.com", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
channel := models.SenlinAgentProjectChannel{ProjectID: project.ID, Title: "Roadmap Board", Icon: "link", URL: "https://example.com/roadmap", SortOrder: 7, CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
plan := models.SenlinAgentCronPlan{ProjectID: project.ID, CreatedBy: 1, Title: "Weekly review", Schedule: "0 9 * * 1", NextRunAt: &nextRun, Enabled: true, CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
inbox := models.SaInboxItem{ProjectID: project.ID, CreatedBy: 1, SourceType: "Manual", Title: "Collect notes", Body: "Turn research into tasks.", Status: "open", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
task := models.SaTask{ProjectID: project.ID, CreatedBy: 1, TagID: &projectTag.ID, Title: "Confirm IA", Description: "Review channel layout.", Status: "done", DueAt: &nextRun, CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
session := models.SaAISession{ProjectID: project.ID, CreatedBy: 1, Title: "UI critique", Context: "Discuss channel behavior.", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
note := models.SaNote{ProjectID: project.ID, CreatedBy: 1, Title: "Workbench principles", Markdown: "# Notes", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
source := models.SaSource{ProjectID: project.ID, CreatedBy: 1, Kind: "link", Title: "Reference board", URL: "https://example.com", CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
channel := models.SaProjectChannel{ProjectID: project.ID, Title: "Roadmap Board", Icon: "link", URL: "https://example.com/roadmap", SortOrder: 7, CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
plan := models.SaCronPlan{ProjectID: project.ID, CreatedBy: 1, Title: "Weekly review", Schedule: "0 9 * * 1", NextRunAt: &nextRun, Enabled: true, CreatedAt: createdAt, UpdatedAt: updatedAt}
|
||||
for _, record := range []any{&inbox, &task, &session, ¬e, &source, &channel, &plan} {
|
||||
require.NoError(t, database.Create(record).Error)
|
||||
}
|
||||
@@ -103,12 +103,12 @@ func TestWorkspaceChannelDTOSerializesRequiredCountAndURL(t *testing.T) {
|
||||
func TestWorkspaceDoneTaskDoesNotInventCompletedAt(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
service := NewService()
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
project, err := service.CreateProject(1, "Alpha", "")
|
||||
require.NoError(t, err)
|
||||
createdAt := time.Date(2026, 7, 20, 1, 0, 0, 0, time.UTC)
|
||||
updatedAt := createdAt.Add(2 * time.Hour)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTask{
|
||||
require.NoError(t, database.Create(&models.SaTask{
|
||||
ProjectID: project.ID, CreatedBy: 1, Title: "Done", Status: "done", CreatedAt: createdAt, UpdatedAt: updatedAt,
|
||||
}).Error)
|
||||
|
||||
|
||||
@@ -18,12 +18,12 @@ import (
|
||||
|
||||
func TestTagRegistrarUsesIdentityAndListsOnlyProjectTags(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
first := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
second := models.SenlinAgentProject{OwnerID: 1, Name: "Beta", Identifier: "BETA"}
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
first := models.SaProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
second := models.SaProject{OwnerID: 1, Name: "Beta", Identifier: "BETA"}
|
||||
require.NoError(t, database.Create(&first).Error)
|
||||
require.NoError(t, database.Create(&second).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTag{ProjectID: second.ID, Name: "其他项目"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaTag{ProjectID: second.ID, Name: "其他项目"}).Error)
|
||||
router := newProjectWriteHandlerRouter(t, 1, NewTagHandler(NewService()))
|
||||
createReq := httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+first.Identity+"/tags", bytes.NewBufferString(`{"name":"设计"}`))
|
||||
createReq.Header.Set("Content-Type", "application/json")
|
||||
@@ -52,8 +52,8 @@ func TestTagRegistrarUsesIdentityAndListsOnlyProjectTags(t *testing.T) {
|
||||
|
||||
func TestTagRegistrarRejectsProjectOwnedByAnotherUser(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
other := models.SenlinAgentProject{OwnerID: 2, Name: "Private", Identifier: "PRIVATE"}
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
other := models.SaProject{OwnerID: 2, Name: "Private", Identifier: "PRIVATE"}
|
||||
require.NoError(t, database.Create(&other).Error)
|
||||
router := newProjectWriteHandlerRouter(t, 1, NewTagHandler(NewService()))
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+other.Identity+"/tags", bytes.NewBufferString(`{"name":"越权"}`))
|
||||
@@ -72,8 +72,8 @@ func TestTagRegistrarRejectsProjectOwnedByAnotherUser(t *testing.T) {
|
||||
|
||||
func TestTagAndCronServicesReturnTypedValidationErrors(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
project := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
project := models.SaProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
service := NewService()
|
||||
|
||||
@@ -87,11 +87,11 @@ func TestTagAndCronServicesReturnTypedValidationErrors(t *testing.T) {
|
||||
|
||||
func TestTagRegistrarMapsDatabaseFailureToInternalError(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
project := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
project := models.SaProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
require.NoError(t, database.Callback().Create().Before("gorm:create").Register("test:fail_tag_create", func(tx *gorm.DB) {
|
||||
if tx.Statement.Schema != nil && tx.Statement.Schema.Table == (models.SenlinAgentTag{}).TableName() {
|
||||
if tx.Statement.Schema != nil && tx.Statement.Schema.Table == (models.SaTag{}).TableName() {
|
||||
tx.AddError(errors.New("forced tag create failure"))
|
||||
}
|
||||
}))
|
||||
@@ -112,11 +112,11 @@ func TestTagRegistrarMapsDatabaseFailureToInternalError(t *testing.T) {
|
||||
|
||||
func TestCronRegistrarMapsDatabaseFailureToInternalError(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
project := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
project := models.SaProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
require.NoError(t, database.Callback().Create().Before("gorm:create").Register("test:fail_cron_create", func(tx *gorm.DB) {
|
||||
if tx.Statement.Schema != nil && tx.Statement.Schema.Table == (models.SenlinAgentCronPlan{}).TableName() {
|
||||
if tx.Statement.Schema != nil && tx.Statement.Schema.Table == (models.SaCronPlan{}).TableName() {
|
||||
tx.AddError(errors.New("forced cron create failure"))
|
||||
}
|
||||
}))
|
||||
@@ -137,8 +137,8 @@ func TestCronRegistrarMapsDatabaseFailureToInternalError(t *testing.T) {
|
||||
|
||||
func TestCronRegistrarCreatesCamelCaseIdentityDTO(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
project := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
project := models.SaProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
router := newProjectWriteHandlerRouter(t, 1, NewCronHandler(NewService()))
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+project.Identity+"/cron-plans", bytes.NewBufferString(`{"title":"每日整理","schedule":"0 9 * * *","enabled":true,"nextRunAt":"2026-07-22T08:00:00Z"}`))
|
||||
@@ -149,7 +149,7 @@ func TestCronRegistrarCreatesCamelCaseIdentityDTO(t *testing.T) {
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusCreated, rec.Code, rec.Body.String())
|
||||
var plan models.SenlinAgentCronPlan
|
||||
var plan models.SaCronPlan
|
||||
require.NoError(t, database.Where("project_id = ?", project.ID).First(&plan).Error)
|
||||
var payload map[string]any
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
|
||||
|
||||
@@ -33,11 +33,11 @@ func TestSearchHandlerRejectsBlankQuery(t *testing.T) {
|
||||
func TestSearchHandlerReturnsCamelCaseIdentityResults(t *testing.T) {
|
||||
database := newSearchHandlerTestDB(t)
|
||||
owner := createSearchUser(t, database, "owner@example.com")
|
||||
project := models.SenlinAgentProject{OwnerID: owner.ID, Name: "回调平台", Identifier: "CALLBACK"}
|
||||
project := models.SaProject{OwnerID: owner.ID, Name: "回调平台", Identifier: "CALLBACK"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
task := models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "回调任务", Description: "检查签名", Status: "open"}
|
||||
task := models.SaTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "回调任务", Description: "检查签名", Status: "open"}
|
||||
require.NoError(t, database.Create(&task).Error)
|
||||
note := models.SenlinAgentNote{ProjectID: project.ID, CreatedBy: owner.ID, Title: "接口说明", Markdown: "回调验签流程"}
|
||||
note := models.SaNote{ProjectID: project.ID, CreatedBy: owner.ID, Title: "接口说明", Markdown: "回调验签流程"}
|
||||
require.NoError(t, database.Create(¬e).Error)
|
||||
router := searchHandlerTestRouter(owner.ID)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/search?q=%E5%9B%9E%E8%B0%83", nil)
|
||||
@@ -73,25 +73,25 @@ func TestSearchHandlerOnlyReturnsObjectsVisibleToCurrentUser(t *testing.T) {
|
||||
database := newSearchHandlerTestDB(t)
|
||||
owner := createSearchUser(t, database, "owner@example.com")
|
||||
viewer := createSearchUser(t, database, "viewer@example.com")
|
||||
project := models.SenlinAgentProject{OwnerID: owner.ID, Name: "保密检索词项目", Identifier: "PRIVATE"}
|
||||
project := models.SaProject{OwnerID: owner.ID, Name: "保密检索词项目", Identifier: "PRIVATE"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
assigned := models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: owner.ID, AssigneeID: &viewer.ID, Title: "检索词已指派任务", Status: "open"}
|
||||
assigned := models.SaTask{ProjectID: project.ID, CreatedBy: owner.ID, AssigneeID: &viewer.ID, Title: "检索词已指派任务", Status: "open"}
|
||||
require.NoError(t, database.Create(&assigned).Error)
|
||||
privateTask := models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "检索词私有任务", Status: "open"}
|
||||
privateTask := models.SaTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "检索词私有任务", Status: "open"}
|
||||
require.NoError(t, database.Create(&privateTask).Error)
|
||||
sharedNote := models.SenlinAgentNote{ProjectID: project.ID, CreatedBy: owner.ID, Title: "检索词共享笔记", Markdown: "已显式分享"}
|
||||
sharedNote := models.SaNote{ProjectID: project.ID, CreatedBy: owner.ID, Title: "检索词共享笔记", Markdown: "已显式分享"}
|
||||
require.NoError(t, database.Create(&sharedNote).Error)
|
||||
privateNote := models.SenlinAgentNote{ProjectID: project.ID, CreatedBy: owner.ID, Title: "检索词私有笔记", Markdown: "不得泄漏"}
|
||||
privateNote := models.SaNote{ProjectID: project.ID, CreatedBy: owner.ID, Title: "检索词私有笔记", Markdown: "不得泄漏"}
|
||||
require.NoError(t, database.Create(&privateNote).Error)
|
||||
otherProject := models.SenlinAgentProject{OwnerID: owner.ID, Name: "其他项目", Identifier: "OTHER"}
|
||||
otherProject := models.SaProject{OwnerID: owner.ID, Name: "其他项目", Identifier: "OTHER"}
|
||||
require.NoError(t, database.Create(&otherProject).Error)
|
||||
crossProjectNote := models.SenlinAgentNote{ProjectID: otherProject.ID, CreatedBy: owner.ID, Title: "检索词跨项目笔记", Markdown: "伪造分享也不得泄漏"}
|
||||
crossProjectNote := models.SaNote{ProjectID: otherProject.ID, CreatedBy: owner.ID, Title: "检索词跨项目笔记", Markdown: "伪造分享也不得泄漏"}
|
||||
require.NoError(t, database.Create(&crossProjectNote).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTaskShare{
|
||||
require.NoError(t, database.Create(&models.SaTaskShare{
|
||||
TaskID: assigned.ID, ObjectType: "note", ObjectID: sharedNote.ID,
|
||||
}).Error)
|
||||
// 即使历史数据绕过服务层写入跨项目分享,搜索也必须在读取边界再次校验项目一致性。
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTaskShare{
|
||||
require.NoError(t, database.Create(&models.SaTaskShare{
|
||||
TaskID: assigned.ID, ObjectType: "note", ObjectID: crossProjectNote.ID,
|
||||
}).Error)
|
||||
router := searchHandlerTestRouter(viewer.ID)
|
||||
@@ -145,9 +145,9 @@ func newSearchHandlerTestDB(t *testing.T) *gorm.DB {
|
||||
return database
|
||||
}
|
||||
|
||||
func createSearchUser(t *testing.T, database *gorm.DB, email string) models.SenlinAgentUser {
|
||||
func createSearchUser(t *testing.T, database *gorm.DB, email string) models.SaUser {
|
||||
t.Helper()
|
||||
user := models.SenlinAgentUser{Email: email, DisplayName: email, PasswordHash: "hash"}
|
||||
user := models.SaUser{Email: email, DisplayName: email, PasswordHash: "hash"}
|
||||
require.NoError(t, database.Create(&user).Error)
|
||||
return user
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func (s *Service) Search(userID uint, query string) ([]SearchResultDTO, error) {
|
||||
operator = "ILIKE"
|
||||
}
|
||||
|
||||
var projects []models.SenlinAgentProject
|
||||
var projects []models.SaProject
|
||||
projectFilter := fmt.Sprintf("owner_id = ? AND (name %s ? ESCAPE '!' OR description %s ? ESCAPE '!')", operator, operator)
|
||||
if err := s.database().Where(projectFilter, userID, like, like).
|
||||
Order("updated_at DESC").Order("identity ASC").
|
||||
@@ -75,11 +75,11 @@ func (s *Service) Search(userID uint, query string) ([]SearchResultDTO, error) {
|
||||
})
|
||||
}
|
||||
|
||||
var tasks []models.SenlinAgentTask
|
||||
taskFilter := fmt.Sprintf("(senlin_agent_projects.owner_id = ? OR senlin_agent_tasks.assignee_id = ?) AND (senlin_agent_tasks.title %s ? ESCAPE '!' OR senlin_agent_tasks.description %s ? ESCAPE '!')", operator, operator)
|
||||
if err := s.database().Select("senlin_agent_tasks.*").Joins("JOIN senlin_agent_projects ON senlin_agent_projects.id = senlin_agent_tasks.project_id").
|
||||
var tasks []models.SaTask
|
||||
taskFilter := fmt.Sprintf("(sa_projects.owner_id = ? OR sa_tasks.assignee_id = ?) AND (sa_tasks.title %s ? ESCAPE '!' OR sa_tasks.description %s ? ESCAPE '!')", operator, operator)
|
||||
if err := s.database().Select("sa_tasks.*").Joins("JOIN sa_projects ON sa_projects.id = sa_tasks.project_id").
|
||||
Where(taskFilter, userID, userID, like, like).
|
||||
Order("senlin_agent_tasks.updated_at DESC").Order("senlin_agent_tasks.identity ASC").
|
||||
Order("sa_tasks.updated_at DESC").Order("sa_tasks.identity ASC").
|
||||
Limit(maxSearchResults).
|
||||
Find(&tasks).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -92,18 +92,18 @@ func (s *Service) Search(userID uint, query string) ([]SearchResultDTO, error) {
|
||||
})
|
||||
}
|
||||
|
||||
var notes []models.SenlinAgentNote
|
||||
noteFilter := fmt.Sprintf(`(senlin_agent_projects.owner_id = ? OR EXISTS (
|
||||
SELECT 1 FROM senlin_agent_task_shares
|
||||
JOIN senlin_agent_tasks ON senlin_agent_tasks.id = senlin_agent_task_shares.task_id
|
||||
WHERE senlin_agent_task_shares.object_type = 'note'
|
||||
AND senlin_agent_task_shares.object_id = senlin_agent_notes.id
|
||||
AND senlin_agent_tasks.project_id = senlin_agent_notes.project_id
|
||||
AND senlin_agent_tasks.assignee_id = ?
|
||||
)) AND (senlin_agent_notes.title %s ? ESCAPE '!' OR senlin_agent_notes.markdown %s ? ESCAPE '!')`, operator, operator)
|
||||
if err := s.database().Select("senlin_agent_notes.*").Joins("JOIN senlin_agent_projects ON senlin_agent_projects.id = senlin_agent_notes.project_id").
|
||||
var notes []models.SaNote
|
||||
noteFilter := fmt.Sprintf(`(sa_projects.owner_id = ? OR EXISTS (
|
||||
SELECT 1 FROM sa_task_shares
|
||||
JOIN sa_tasks ON sa_tasks.id = sa_task_shares.task_id
|
||||
WHERE sa_task_shares.object_type = 'note'
|
||||
AND sa_task_shares.object_id = sa_notes.id
|
||||
AND sa_tasks.project_id = sa_notes.project_id
|
||||
AND sa_tasks.assignee_id = ?
|
||||
)) AND (sa_notes.title %s ? ESCAPE '!' OR sa_notes.markdown %s ? ESCAPE '!')`, operator, operator)
|
||||
if err := s.database().Select("sa_notes.*").Joins("JOIN sa_projects ON sa_projects.id = sa_notes.project_id").
|
||||
Where(noteFilter, userID, userID, like, like).
|
||||
Order("senlin_agent_notes.updated_at DESC").Order("senlin_agent_notes.identity ASC").
|
||||
Order("sa_notes.updated_at DESC").Order("sa_notes.identity ASC").
|
||||
Limit(maxSearchResults).
|
||||
Find(¬es).Error; err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -18,10 +18,10 @@ import (
|
||||
func TestPostgresSearchMatchesChinesePartialKeywords(t *testing.T) {
|
||||
database := newPostgresSearchTestDB(t)
|
||||
owner := createSearchUser(t, database, "postgres-search-owner@example.com")
|
||||
project := models.SenlinAgentProject{OwnerID: owner.ID, Name: "中文回调平台", Identifier: "PG-CALLBACK"}
|
||||
project := models.SaProject{OwnerID: owner.ID, Name: "中文回调平台", Identifier: "PG-CALLBACK"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "回调任务", Status: "open"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentNote{ProjectID: project.ID, CreatedBy: owner.ID, Title: "说明", Markdown: "回调验签流程"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "回调任务", Status: "open"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaNote{ProjectID: project.ID, CreatedBy: owner.ID, Title: "说明", Markdown: "回调验签流程"}).Error)
|
||||
|
||||
results, err := NewService(database).Search(owner.ID, "回调")
|
||||
|
||||
@@ -33,15 +33,15 @@ func TestPostgresSearchKeepsAssignmentAndExplicitShareBoundaries(t *testing.T) {
|
||||
database := newPostgresSearchTestDB(t)
|
||||
owner := createSearchUser(t, database, "postgres-privacy-owner@example.com")
|
||||
viewer := createSearchUser(t, database, "postgres-privacy-viewer@example.com")
|
||||
project := models.SenlinAgentProject{OwnerID: owner.ID, Name: "保密项目", Identifier: "PG-PRIVATE"}
|
||||
project := models.SaProject{OwnerID: owner.ID, Name: "保密项目", Identifier: "PG-PRIVATE"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
assigned := models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: owner.ID, AssigneeID: &viewer.ID, Title: "边界词已指派", Status: "open"}
|
||||
assigned := models.SaTask{ProjectID: project.ID, CreatedBy: owner.ID, AssigneeID: &viewer.ID, Title: "边界词已指派", Status: "open"}
|
||||
require.NoError(t, database.Create(&assigned).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "边界词私有任务", Status: "open"}).Error)
|
||||
sharedNote := models.SenlinAgentNote{ProjectID: project.ID, CreatedBy: owner.ID, Title: "边界词共享笔记"}
|
||||
require.NoError(t, database.Create(&models.SaTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "边界词私有任务", Status: "open"}).Error)
|
||||
sharedNote := models.SaNote{ProjectID: project.ID, CreatedBy: owner.ID, Title: "边界词共享笔记"}
|
||||
require.NoError(t, database.Create(&sharedNote).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentNote{ProjectID: project.ID, CreatedBy: owner.ID, Title: "边界词私有笔记"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTaskShare{TaskID: assigned.ID, ObjectType: "note", ObjectID: sharedNote.ID}).Error)
|
||||
require.NoError(t, database.Create(&models.SaNote{ProjectID: project.ID, CreatedBy: owner.ID, Title: "边界词私有笔记"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaTaskShare{TaskID: assigned.ID, ObjectType: "note", ObjectID: sharedNote.ID}).Error)
|
||||
|
||||
results, err := NewService(database).Search(viewer.ID, "边界词")
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestPostgresSearchUsesStableFairLimitAndRuneBoundedSnippets(t *testing.T) {
|
||||
owner := createSearchUser(t, database, "postgres-search-order@example.com")
|
||||
baseTime := time.Date(2026, time.July, 21, 12, 0, 0, 0, time.UTC)
|
||||
for index := 0; index < 60; index++ {
|
||||
project := models.SenlinAgentProject{
|
||||
project := models.SaProject{
|
||||
Identity: fmt.Sprintf("00000000-0000-7001-8000-%012d", index),
|
||||
OwnerID: owner.ID,
|
||||
Name: fmt.Sprintf("稳定排序项目 %02d", index),
|
||||
@@ -63,7 +63,7 @@ func TestPostgresSearchUsesStableFairLimitAndRuneBoundedSnippets(t *testing.T) {
|
||||
UpdatedAt: baseTime.Add(time.Duration(index) * time.Minute),
|
||||
}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTask{
|
||||
require.NoError(t, database.Create(&models.SaTask{
|
||||
Identity: fmt.Sprintf("00000000-0000-7002-8000-%012d", index),
|
||||
ProjectID: project.ID,
|
||||
CreatedBy: owner.ID,
|
||||
@@ -71,7 +71,7 @@ func TestPostgresSearchUsesStableFairLimitAndRuneBoundedSnippets(t *testing.T) {
|
||||
Status: "open",
|
||||
UpdatedAt: baseTime.Add(time.Duration(index) * time.Minute),
|
||||
}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentNote{
|
||||
require.NoError(t, database.Create(&models.SaNote{
|
||||
Identity: fmt.Sprintf("00000000-0000-7003-8000-%012d", index),
|
||||
ProjectID: project.ID,
|
||||
CreatedBy: owner.ID,
|
||||
|
||||
@@ -17,8 +17,8 @@ func TestSearchFindsNoteBody(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, models.AutoMigrate(database))
|
||||
models.DBService = database
|
||||
require.NoError(t, database.Create(&models.SenlinAgentProject{ID: 1, OwnerID: 7, Name: "支付项目"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentNote{ProjectID: 1, CreatedBy: 7, Title: "接口方案", Markdown: "二维码支付回调设计"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaProject{ID: 1, OwnerID: 7, Name: "支付项目"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaNote{ProjectID: 1, CreatedBy: 7, Title: "接口方案", Markdown: "二维码支付回调设计"}).Error)
|
||||
|
||||
service := NewService()
|
||||
results, err := service.Search(7, "回调")
|
||||
@@ -36,7 +36,7 @@ func TestSearchAppliesStableFairGlobalLimitAcrossResultTypes(t *testing.T) {
|
||||
|
||||
baseTime := time.Date(2026, time.July, 21, 12, 0, 0, 0, time.UTC)
|
||||
for index := 0; index < 60; index++ {
|
||||
project := models.SenlinAgentProject{
|
||||
project := models.SaProject{
|
||||
Identity: fmt.Sprintf("00000000-0000-7001-8000-%012d", index),
|
||||
OwnerID: 7,
|
||||
Name: fmt.Sprintf("稳定排序项目 %02d", index),
|
||||
@@ -44,7 +44,7 @@ func TestSearchAppliesStableFairGlobalLimitAcrossResultTypes(t *testing.T) {
|
||||
UpdatedAt: baseTime.Add(time.Duration(index) * time.Minute),
|
||||
}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTask{
|
||||
require.NoError(t, database.Create(&models.SaTask{
|
||||
Identity: fmt.Sprintf("00000000-0000-7002-8000-%012d", index),
|
||||
ProjectID: project.ID,
|
||||
CreatedBy: 7,
|
||||
@@ -52,7 +52,7 @@ func TestSearchAppliesStableFairGlobalLimitAcrossResultTypes(t *testing.T) {
|
||||
Status: "open",
|
||||
UpdatedAt: baseTime.Add(time.Duration(index) * time.Minute),
|
||||
}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentNote{
|
||||
require.NoError(t, database.Create(&models.SaNote{
|
||||
Identity: fmt.Sprintf("00000000-0000-7003-8000-%012d", index),
|
||||
ProjectID: project.ID,
|
||||
CreatedBy: 7,
|
||||
@@ -89,9 +89,9 @@ func TestSearchBoundsChineseSnippetsByRune(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, models.AutoMigrate(database))
|
||||
models.DBService = database
|
||||
project := models.SenlinAgentProject{OwnerID: 7, Name: "摘要项目", Identifier: "SNIPPET"}
|
||||
project := models.SaProject{OwnerID: 7, Name: "摘要项目", Identifier: "SNIPPET"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentNote{
|
||||
require.NoError(t, database.Create(&models.SaNote{
|
||||
ProjectID: project.ID,
|
||||
CreatedBy: 7,
|
||||
Title: "中文摘要",
|
||||
@@ -118,12 +118,12 @@ func TestSearchOnlyReturnsNavigableProjectTaskAndNoteResults(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, models.AutoMigrate(database))
|
||||
models.DBService = database
|
||||
require.NoError(t, database.Create(&models.SenlinAgentProject{ID: 1, OwnerID: 7, Name: "webhook 支付项目"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTask{ProjectID: 1, CreatedBy: 7, Title: "回调任务", Description: "检查 webhook"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentNote{ProjectID: 1, CreatedBy: 7, Title: "回调笔记", Markdown: "webhook 流程"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentSource{ProjectID: 1, CreatedBy: 7, Kind: "link", Title: "支付文档", URL: "https://example.com/pay", ContentText: "webhook 签名"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentInboxItem{ProjectID: 1, CreatedBy: 7, SourceType: "text", Title: "收集项", Body: "webhook 待整理"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentAISession{ProjectID: 1, CreatedBy: 7, Title: "AI 分析", Context: "webhook 问答"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaProject{ID: 1, OwnerID: 7, Name: "webhook 支付项目"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaTask{ProjectID: 1, CreatedBy: 7, Title: "回调任务", Description: "检查 webhook"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaNote{ProjectID: 1, CreatedBy: 7, Title: "回调笔记", Markdown: "webhook 流程"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaSource{ProjectID: 1, CreatedBy: 7, Kind: "link", Title: "支付文档", URL: "https://example.com/pay", ContentText: "webhook 签名"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaInboxItem{ProjectID: 1, CreatedBy: 7, SourceType: "text", Title: "收集项", Body: "webhook 待整理"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaAISession{ProjectID: 1, CreatedBy: 7, Title: "AI 分析", Context: "webhook 问答"}).Error)
|
||||
|
||||
service := NewService()
|
||||
results, err := service.Search(7, "webhook")
|
||||
@@ -142,8 +142,8 @@ func TestSearchTreatsLikeWildcardsAsLiteralKeywords(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, models.AutoMigrate(database))
|
||||
models.DBService = database
|
||||
require.NoError(t, database.Create(&models.SenlinAgentProject{ID: 1, OwnerID: 7, Name: "100%_完成", Identifier: "SPECIAL"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentProject{ID: 2, OwnerID: 7, Name: "普通项目", Identifier: "NORMAL"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaProject{ID: 1, OwnerID: 7, Name: "100%_完成", Identifier: "SPECIAL"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaProject{ID: 2, OwnerID: 7, Name: "普通项目", Identifier: "NORMAL"}).Error)
|
||||
|
||||
percentResults, err := NewService().Search(7, "%")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -22,19 +22,19 @@ func TestPostgresMovePreventsOldProjectShareFromBeingInsertedConcurrently(t *tes
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, models.AutoMigrate(database))
|
||||
suffix := fmt.Sprint(time.Now().UnixNano())
|
||||
owner := models.SenlinAgentUser{Email: "lock-" + suffix + "@example.com", DisplayName: "Lock Owner", PasswordHash: "hash"}
|
||||
owner := models.SaUser{Email: "lock-" + suffix + "@example.com", DisplayName: "Lock Owner", PasswordHash: "hash"}
|
||||
require.NoError(t, database.Create(&owner).Error)
|
||||
first := models.SenlinAgentProject{OwnerID: owner.ID, Name: "First", Identifier: "FIRST-" + suffix}
|
||||
second := models.SenlinAgentProject{OwnerID: owner.ID, Name: "Second", Identifier: "SECOND-" + suffix}
|
||||
first := models.SaProject{OwnerID: owner.ID, Name: "First", Identifier: "FIRST-" + suffix}
|
||||
second := models.SaProject{OwnerID: owner.ID, Name: "Second", Identifier: "SECOND-" + suffix}
|
||||
require.NoError(t, database.Create(&first).Error)
|
||||
require.NoError(t, database.Create(&second).Error)
|
||||
task := models.SenlinAgentTask{ProjectID: first.ID, CreatedBy: owner.ID, Title: "Move", Status: "open"}
|
||||
note := models.SenlinAgentNote{ProjectID: first.ID, CreatedBy: owner.ID, Title: "Old context", Markdown: "private"}
|
||||
task := models.SaTask{ProjectID: first.ID, CreatedBy: owner.ID, Title: "Move", Status: "open"}
|
||||
note := models.SaNote{ProjectID: first.ID, CreatedBy: owner.ID, Title: "Old context", Markdown: "private"}
|
||||
require.NoError(t, database.Create(&task).Error)
|
||||
require.NoError(t, database.Create(¬e).Error)
|
||||
t.Cleanup(func() {
|
||||
database.Where("task_id = ?", task.ID).Delete(&models.SenlinAgentTaskShare{})
|
||||
database.Where("entity_type = ? AND entity_id = ?", "task", task.ID).Delete(&models.SenlinAgentProjectEvent{})
|
||||
database.Where("task_id = ?", task.ID).Delete(&models.SaTaskShare{})
|
||||
database.Where("entity_type = ? AND entity_id = ?", "task", task.ID).Delete(&models.SaProjectEvent{})
|
||||
database.Delete(¬e)
|
||||
database.Delete(&task)
|
||||
database.Delete(&first)
|
||||
@@ -50,7 +50,7 @@ func TestPostgresMovePreventsOldProjectShareFromBeingInsertedConcurrently(t *tes
|
||||
t.Cleanup(releaseLock)
|
||||
callbackName := "test:pause_first_task_lock_" + suffix
|
||||
require.NoError(t, database.Callback().Query().After("gorm:query").Register(callbackName, func(tx *gorm.DB) {
|
||||
if tx.Statement.Schema == nil || tx.Statement.Schema.Table != (models.SenlinAgentTask{}).TableName() {
|
||||
if tx.Statement.Schema == nil || tx.Statement.Schema.Table != (models.SaTask{}).TableName() {
|
||||
return
|
||||
}
|
||||
if _, ok := tx.Statement.Clauses["FOR"]; !ok {
|
||||
@@ -89,6 +89,6 @@ func TestPostgresMovePreventsOldProjectShareFromBeingInsertedConcurrently(t *tes
|
||||
require.ErrorContains(t, <-shareResult, "shared object not found in task project")
|
||||
|
||||
var shareCount int64
|
||||
require.NoError(t, database.Model(&models.SenlinAgentTaskShare{}).Where("task_id = ?", task.ID).Count(&shareCount).Error)
|
||||
require.NoError(t, database.Model(&models.SaTaskShare{}).Where("task_id = ?", task.ID).Count(&shareCount).Error)
|
||||
require.Zero(t, shareCount)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestTaskRegistrarCreatesIdentityDTOForOwnedProject(t *testing.T) {
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusCreated, rec.Code, rec.Body.String())
|
||||
var task models.SenlinAgentTask
|
||||
var task models.SaTask
|
||||
require.NoError(t, database.Where("project_id = ?", project.ID).First(&task).Error)
|
||||
var payload map[string]any
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
|
||||
@@ -44,13 +44,13 @@ func TestTaskRegistrarCreatesIdentityDTOForOwnedProject(t *testing.T) {
|
||||
|
||||
func TestTaskRegistrarMovesTaskByIdentityAndClearsForeignProjectTag(t *testing.T) {
|
||||
router, database, first := newTaskHandlerTestRouter(t, 1)
|
||||
second := models.SenlinAgentProject{OwnerID: 1, Name: "Beta", Identifier: "BETA"}
|
||||
second := models.SaProject{OwnerID: 1, Name: "Beta", Identifier: "BETA"}
|
||||
require.NoError(t, database.Create(&second).Error)
|
||||
tag := models.SenlinAgentTag{ProjectID: first.ID, Name: "仅 Alpha"}
|
||||
tag := models.SaTag{ProjectID: first.ID, Name: "仅 Alpha"}
|
||||
require.NoError(t, database.Create(&tag).Error)
|
||||
task := models.SenlinAgentTask{ProjectID: first.ID, CreatedBy: 1, TagID: &tag.ID, Title: "迁移任务", Status: "open"}
|
||||
task := models.SaTask{ProjectID: first.ID, CreatedBy: 1, TagID: &tag.ID, Title: "迁移任务", Status: "open"}
|
||||
require.NoError(t, database.Create(&task).Error)
|
||||
note := models.SenlinAgentNote{ProjectID: first.ID, CreatedBy: 1, Title: "旧项目资料", Markdown: "仅可在 Alpha 分享"}
|
||||
note := models.SaNote{ProjectID: first.ID, CreatedBy: 1, Title: "旧项目资料", Markdown: "仅可在 Alpha 分享"}
|
||||
require.NoError(t, database.Create(¬e).Error)
|
||||
require.NoError(t, NewService(database).ShareObject(task.ID, "note", note.ID))
|
||||
body := bytes.NewBufferString(fmt.Sprintf(`{"title":"迁移任务","description":"已移动","completed":false,"nextProjectId":%q}`, strings.ToUpper(second.Identity)))
|
||||
@@ -66,7 +66,7 @@ func TestTaskRegistrarMovesTaskByIdentityAndClearsForeignProjectTag(t *testing.T
|
||||
require.Equal(t, second.ID, task.ProjectID)
|
||||
require.Nil(t, task.TagID)
|
||||
var shareCount int64
|
||||
require.NoError(t, database.Model(&models.SenlinAgentTaskShare{}).Where("task_id = ?", task.ID).Count(&shareCount).Error)
|
||||
require.NoError(t, database.Model(&models.SaTaskShare{}).Where("task_id = ?", task.ID).Count(&shareCount).Error)
|
||||
require.Zero(t, shareCount, "移动项目后不能保留旧项目的显式分享")
|
||||
var payload map[string]any
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
|
||||
@@ -76,7 +76,7 @@ func TestTaskRegistrarMovesTaskByIdentityAndClearsForeignProjectTag(t *testing.T
|
||||
|
||||
func TestTaskRegistrarRejectsProjectOwnedByAnotherUser(t *testing.T) {
|
||||
router, database, _ := newTaskHandlerTestRouter(t, 1)
|
||||
other := models.SenlinAgentProject{OwnerID: 2, Name: "Private", Identifier: "PRIVATE"}
|
||||
other := models.SaProject{OwnerID: 2, Name: "Private", Identifier: "PRIVATE"}
|
||||
require.NoError(t, database.Create(&other).Error)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+other.Identity+"/tasks", bytes.NewBufferString(`{"title":"越权"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
@@ -87,19 +87,19 @@ func TestTaskRegistrarRejectsProjectOwnedByAnotherUser(t *testing.T) {
|
||||
|
||||
require.Equal(t, http.StatusNotFound, rec.Code, rec.Body.String())
|
||||
var count int64
|
||||
require.NoError(t, database.Model(&models.SenlinAgentTask{}).Where("project_id = ?", other.ID).Count(&count).Error)
|
||||
require.NoError(t, database.Model(&models.SaTask{}).Where("project_id = ?", other.ID).Count(&count).Error)
|
||||
require.Zero(t, count)
|
||||
}
|
||||
|
||||
func newTaskHandlerTestRouter(t *testing.T, currentUserID uint) (*gin.Engine, *gorm.DB, models.SenlinAgentProject) {
|
||||
func newTaskHandlerTestRouter(t *testing.T, currentUserID uint) (*gin.Engine, *gorm.DB, models.SaProject) {
|
||||
t.Helper()
|
||||
database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{TranslateError: true})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, models.AutoMigrate(database))
|
||||
models.DBService = database
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "other@example.com", DisplayName: "Other", PasswordHash: "hash"}).Error)
|
||||
project := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
require.NoError(t, database.Create(&models.SaUser{Email: "other@example.com", DisplayName: "Other", PasswordHash: "hash"}).Error)
|
||||
project := models.SaProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
router := httpx.NewProtectedRouter(
|
||||
config.Config{Env: "test"},
|
||||
|
||||
@@ -39,11 +39,11 @@ func (s *Service) database() *gorm.DB {
|
||||
// Assign 在同一事务内由用户 identity 解析内部主键,并同步两个指派字段,避免 DTO 返回陈旧 identity。
|
||||
func (s *Service) Assign(taskID uint, assigneeIdentity string) error {
|
||||
return s.database().Transaction(func(tx *gorm.DB) error {
|
||||
var task models.SenlinAgentTask
|
||||
var task models.SaTask
|
||||
if err := tx.First(&task, taskID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var assignee models.SenlinAgentUser
|
||||
var assignee models.SaUser
|
||||
if err := tx.Where("identity = ?", assigneeIdentity).First(&assignee).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func (s *Service) Assign(taskID uint, assigneeIdentity string) error {
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&models.SenlinAgentProjectEvent{
|
||||
return tx.Create(&models.SaProjectEvent{
|
||||
ProjectID: task.ProjectID,
|
||||
ActorID: task.CreatedBy,
|
||||
EventType: "task_assigned",
|
||||
@@ -76,10 +76,10 @@ func (s *Service) ShareObject(taskID uint, objectType string, objectID uint) err
|
||||
if err := ensureSharedObjectInProject(tx, task.ProjectID, objectType, objectID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&models.SenlinAgentTaskShare{TaskID: taskID, ObjectType: objectType, ObjectID: objectID}).Error; err != nil {
|
||||
if err := tx.Create(&models.SaTaskShare{TaskID: taskID, ObjectType: objectType, ObjectID: objectID}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&models.SenlinAgentProjectEvent{
|
||||
return tx.Create(&models.SaProjectEvent{
|
||||
ProjectID: task.ProjectID,
|
||||
ActorID: task.CreatedBy,
|
||||
EventType: "task_object_shared",
|
||||
@@ -92,14 +92,14 @@ func (s *Service) ShareObject(taskID uint, objectType string, objectID uint) err
|
||||
|
||||
// VisibleLinkedObjects 对被指派人也只返回显式分享记录,不因任务可见而扩大关联对象权限。
|
||||
func (s *Service) VisibleLinkedObjects(taskID uint, viewerID uint) ([]LinkedObject, error) {
|
||||
var task models.SenlinAgentTask
|
||||
var task models.SaTask
|
||||
if err := s.database().First(&task, taskID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if task.AssigneeID == nil || *task.AssigneeID != viewerID {
|
||||
return []LinkedObject{}, nil
|
||||
}
|
||||
var shares []models.SenlinAgentTaskShare
|
||||
var shares []models.SaTaskShare
|
||||
if err := s.database().Where("task_id = ?", taskID).Find(&shares).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -132,7 +132,7 @@ func (s *Service) Create(ownerID uint, projectIdentity string, input CreateTaskI
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task := models.SenlinAgentTask{
|
||||
task := models.SaTask{
|
||||
ProjectID: project.ID, CreatedBy: ownerID, TagID: tagID,
|
||||
Title: title, Description: strings.TrimSpace(input.Description), Status: status, DueAt: utcOptionalTime(input.DueAt),
|
||||
}
|
||||
@@ -194,7 +194,7 @@ func (s *Service) Update(ownerID uint, projectIdentity, taskIdentity string, inp
|
||||
task.TagIdentity = tagIdentity
|
||||
if targetProject.ID != currentProject.ID {
|
||||
// 现有分享都在原项目边界内;移动后必须清空,避免旧项目 note/source 继续对被指派人可见。
|
||||
if err := tx.Where("task_id = ?", task.ID).Delete(&models.SenlinAgentTaskShare{}).Error; err != nil {
|
||||
if err := tx.Where("task_id = ?", task.ID).Delete(&models.SaTaskShare{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -207,24 +207,24 @@ func (s *Service) Update(ownerID uint, projectIdentity, taskIdentity string, inp
|
||||
return result, err
|
||||
}
|
||||
|
||||
func lockTaskByID(tx *gorm.DB, taskID uint) (*models.SenlinAgentTask, error) {
|
||||
var task models.SenlinAgentTask
|
||||
func lockTaskByID(tx *gorm.DB, taskID uint) (*models.SaTask, error) {
|
||||
var task models.SaTask
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&task, taskID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
func lockTaskByIdentity(tx *gorm.DB, taskIdentity string) (*models.SenlinAgentTask, error) {
|
||||
var task models.SenlinAgentTask
|
||||
func lockTaskByIdentity(tx *gorm.DB, taskIdentity string) (*models.SaTask, error) {
|
||||
var task models.SaTask
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", taskIdentity).First(&task).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
func findOwnedProject(tx *gorm.DB, ownerID uint, identity string) (*models.SenlinAgentProject, error) {
|
||||
var project models.SenlinAgentProject
|
||||
func findOwnedProject(tx *gorm.DB, ownerID uint, identity string) (*models.SaProject, error) {
|
||||
var project models.SaProject
|
||||
if err := tx.Where("owner_id = ? AND identity = ?", ownerID, identity).First(&project).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -236,21 +236,21 @@ func findOrCreateProjectTag(tx *gorm.DB, projectID uint, value string) (*uint, *
|
||||
if name == "" {
|
||||
return nil, nil, "", nil
|
||||
}
|
||||
candidate := models.SenlinAgentTag{ProjectID: projectID, Name: name}
|
||||
candidate := models.SaTag{ProjectID: projectID, Name: name}
|
||||
if err := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "project_id"}, {Name: "name"}},
|
||||
DoNothing: true,
|
||||
}).Create(&candidate).Error; err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
var tag models.SenlinAgentTag
|
||||
var tag models.SaTag
|
||||
if err := tx.Where("project_id = ? AND name = ?", projectID, name).First(&tag).Error; err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
return &tag.ID, &tag.Identity, tag.Name, nil
|
||||
}
|
||||
|
||||
func makeTaskDTO(task models.SenlinAgentTask, projectIdentity string, tagIdentity *string, tagName string) TaskDTO {
|
||||
func makeTaskDTO(task models.SaTask, projectIdentity string, tagIdentity *string, tagName string) TaskDTO {
|
||||
return TaskDTO{
|
||||
ID: task.Identity, ProjectID: projectIdentity, Title: task.Title, Description: task.Description,
|
||||
Status: task.Status, Completed: task.Status == "done", DueAt: utcOptionalTime(task.DueAt),
|
||||
@@ -271,7 +271,7 @@ func ensureSharedObjectInProject(tx *gorm.DB, projectID uint, objectType string,
|
||||
switch objectType {
|
||||
case "note":
|
||||
var count int64
|
||||
if err := tx.Model(&models.SenlinAgentNote{}).Where("id = ? AND project_id = ?", objectID, projectID).Count(&count).Error; err != nil {
|
||||
if err := tx.Model(&models.SaNote{}).Where("id = ? AND project_id = ?", objectID, projectID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
@@ -279,7 +279,7 @@ func ensureSharedObjectInProject(tx *gorm.DB, projectID uint, objectType string,
|
||||
}
|
||||
case "source":
|
||||
var count int64
|
||||
if err := tx.Model(&models.SenlinAgentSource{}).Where("id = ? AND project_id = ?", objectID, projectID).Count(&count).Error; err != nil {
|
||||
if err := tx.Model(&models.SaSource{}).Where("id = ? AND project_id = ?", objectID, projectID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestUpdateLocksTaskBeforeProjectValidation(t *testing.T) {
|
||||
|
||||
func TestShareObjectLocksTaskBeforeObjectValidation(t *testing.T) {
|
||||
database, _, project, task := newTaskLockFixture(t)
|
||||
note := models.SenlinAgentNote{ProjectID: project.ID, CreatedBy: task.CreatedBy, Title: "Context", Markdown: "Private"}
|
||||
note := models.SaNote{ProjectID: project.ID, CreatedBy: task.CreatedBy, Title: "Context", Markdown: "Private"}
|
||||
require.NoError(t, database.Create(¬e).Error)
|
||||
queries := captureTaskQueryOrder(t, database)
|
||||
|
||||
@@ -38,8 +38,8 @@ func TestShareObjectLocksTaskBeforeObjectValidation(t *testing.T) {
|
||||
func TestAssigneeOnlySeesExplicitlySharedObjects(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
assigneeID := uint(2)
|
||||
task := models.SenlinAgentTask{ProjectID: 1, CreatedBy: 1, AssigneeID: &assigneeID, Title: "处理合同"}
|
||||
note := models.SenlinAgentNote{ProjectID: 1, CreatedBy: 1, Title: "合同背景", Markdown: "只在共享后可见"}
|
||||
task := models.SaTask{ProjectID: 1, CreatedBy: 1, AssigneeID: &assigneeID, Title: "处理合同"}
|
||||
note := models.SaNote{ProjectID: 1, CreatedBy: 1, Title: "合同背景", Markdown: "只在共享后可见"}
|
||||
require.NoError(t, database.Create(&task).Error)
|
||||
require.NoError(t, database.Create(¬e).Error)
|
||||
service := NewService()
|
||||
@@ -67,8 +67,8 @@ func TestShareObjectRejectsUnsupportedType(t *testing.T) {
|
||||
|
||||
func TestShareObjectRejectsObjectFromAnotherProject(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
task := models.SenlinAgentTask{ProjectID: 1, CreatedBy: 1, Title: "Review"}
|
||||
note := models.SenlinAgentNote{ProjectID: 2, CreatedBy: 1, Title: "Other project", Markdown: "Private context"}
|
||||
task := models.SaTask{ProjectID: 1, CreatedBy: 1, Title: "Review"}
|
||||
note := models.SaNote{ProjectID: 2, CreatedBy: 1, Title: "Other project", Markdown: "Private context"}
|
||||
require.NoError(t, database.Create(&task).Error)
|
||||
require.NoError(t, database.Create(¬e).Error)
|
||||
service := NewService()
|
||||
@@ -80,15 +80,15 @@ func TestShareObjectRejectsObjectFromAnotherProject(t *testing.T) {
|
||||
|
||||
func TestAssignResolvesIdentityAndTaskDTOReflectsReassignment(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
owner := models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}
|
||||
first := models.SenlinAgentUser{Email: "first@example.com", DisplayName: "First", PasswordHash: "hash"}
|
||||
second := models.SenlinAgentUser{Email: "second@example.com", DisplayName: "Second", PasswordHash: "hash"}
|
||||
owner := models.SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}
|
||||
first := models.SaUser{Email: "first@example.com", DisplayName: "First", PasswordHash: "hash"}
|
||||
second := models.SaUser{Email: "second@example.com", DisplayName: "Second", PasswordHash: "hash"}
|
||||
require.NoError(t, database.Create(&owner).Error)
|
||||
require.NoError(t, database.Create(&first).Error)
|
||||
require.NoError(t, database.Create(&second).Error)
|
||||
project := models.SenlinAgentProject{OwnerID: owner.ID, Name: "Alpha", Identifier: "ALPHA"}
|
||||
project := models.SaProject{OwnerID: owner.ID, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
task := models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "安排评审", Status: "open"}
|
||||
task := models.SaTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "安排评审", Status: "open"}
|
||||
require.NoError(t, database.Create(&task).Error)
|
||||
service := NewService(database)
|
||||
|
||||
@@ -102,7 +102,7 @@ func TestAssignResolvesIdentityAndTaskDTOReflectsReassignment(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, second.Identity, *reassigned.AssigneeID)
|
||||
|
||||
var event models.SenlinAgentProjectEvent
|
||||
var event models.SaProjectEvent
|
||||
require.NoError(t, database.Where("project_id = ? AND entity_type = ? AND entity_id = ?", project.ID, "task", task.ID).Order("id desc").First(&event).Error)
|
||||
require.Equal(t, "task_assigned", event.EventType)
|
||||
}
|
||||
@@ -116,14 +116,14 @@ func newTestDB(t *testing.T) *gorm.DB {
|
||||
return database
|
||||
}
|
||||
|
||||
func newTaskLockFixture(t *testing.T) (*gorm.DB, models.SenlinAgentUser, models.SenlinAgentProject, models.SenlinAgentTask) {
|
||||
func newTaskLockFixture(t *testing.T) (*gorm.DB, models.SaUser, models.SaProject, models.SaTask) {
|
||||
t.Helper()
|
||||
database := newTestDB(t)
|
||||
owner := models.SenlinAgentUser{Email: "lock-owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}
|
||||
owner := models.SaUser{Email: "lock-owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}
|
||||
require.NoError(t, database.Create(&owner).Error)
|
||||
project := models.SenlinAgentProject{OwnerID: owner.ID, Name: "Lock", Identifier: "LOCK"}
|
||||
project := models.SaProject{OwnerID: owner.ID, Name: "Lock", Identifier: "LOCK"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
task := models.SenlinAgentTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "Lock me", Status: "open"}
|
||||
task := models.SaTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "Lock me", Status: "open"}
|
||||
require.NoError(t, database.Create(&task).Error)
|
||||
return database, owner, project, task
|
||||
}
|
||||
@@ -138,11 +138,11 @@ func captureTaskQueryOrder(t *testing.T, database *gorm.DB) *[]string {
|
||||
if tx.Statement.Schema != nil {
|
||||
table = tx.Statement.Schema.Table
|
||||
}
|
||||
if table != (models.SenlinAgentTask{}).TableName() && table != (models.SenlinAgentProject{}).TableName() {
|
||||
if table != (models.SaTask{}).TableName() && table != (models.SaProject{}).TableName() {
|
||||
return
|
||||
}
|
||||
entry := "project"
|
||||
if table == (models.SenlinAgentTask{}).TableName() {
|
||||
if table == (models.SaTask{}).TableName() {
|
||||
entry = "task"
|
||||
if _, ok := tx.Statement.Clauses["FOR"]; ok {
|
||||
entry += ":locked"
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
type SenlinAgentAICallLog struct {
|
||||
type SaAICallLog struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
UserID uint `gorm:"index;not null"`
|
||||
@@ -15,6 +15,6 @@ type SenlinAgentAICallLog struct {
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentAICallLog) TableName() string {
|
||||
return "senlin_agent_ai_call_logs"
|
||||
func (SaAICallLog) TableName() string {
|
||||
return "sa_ai_call_logs"
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
// SenlinAgentAIExpertCategory groups experts from the local expert catalog.
|
||||
type SenlinAgentAIExpertCategory struct {
|
||||
// SaAIExpertCategory groups experts from the local expert catalog.
|
||||
type SaAIExpertCategory struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
Slug string `gorm:"size:80;uniqueIndex;not null"`
|
||||
@@ -12,6 +12,6 @@ type SenlinAgentAIExpertCategory struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentAIExpertCategory) TableName() string {
|
||||
return "senlin_agent_ai_expert_categories"
|
||||
func (SaAIExpertCategory) TableName() string {
|
||||
return "sa_ai_expert_categories"
|
||||
}
|
||||
|
||||
@@ -2,29 +2,29 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
// SenlinAgentAIExpertItem 是本地专家库中的可选 AI 角色。
|
||||
type SenlinAgentAIExpertItem struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
Slug string `gorm:"size:160;uniqueIndex;not null"`
|
||||
CategoryID uint `gorm:"index"`
|
||||
CategoryIdentity string `gorm:"type:char(36);index"`
|
||||
ExpertCategory SenlinAgentAIExpertCategory `gorm:"foreignKey:CategoryID"`
|
||||
Category string `gorm:"size:80;index;not null"`
|
||||
CategoryName string `gorm:"size:120;not null"`
|
||||
Name string `gorm:"size:160;index;not null"`
|
||||
Description string `gorm:"type:text;not null"`
|
||||
Emoji string `gorm:"size:32"`
|
||||
Color string `gorm:"size:32"`
|
||||
SystemPrompt string `gorm:"type:text;not null"`
|
||||
Source string `gorm:"size:255;not null"`
|
||||
Reference string `gorm:"size:255"`
|
||||
SourceLicense string `gorm:"size:32;not null"`
|
||||
Enabled bool `gorm:"not null;default:true;index"`
|
||||
// SaAIExpertItem 是本地专家库中的可选 AI 角色。
|
||||
type SaAIExpertItem struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
Slug string `gorm:"size:160;uniqueIndex;not null"`
|
||||
CategoryID uint `gorm:"index"`
|
||||
CategoryIdentity string `gorm:"type:char(36);index"`
|
||||
ExpertCategory SaAIExpertCategory `gorm:"foreignKey:CategoryID"`
|
||||
Category string `gorm:"size:80;index;not null"`
|
||||
CategoryName string `gorm:"size:120;not null"`
|
||||
Name string `gorm:"size:160;index;not null"`
|
||||
Description string `gorm:"type:text;not null"`
|
||||
Emoji string `gorm:"size:32"`
|
||||
Color string `gorm:"size:32"`
|
||||
SystemPrompt string `gorm:"type:text;not null"`
|
||||
Source string `gorm:"size:255;not null"`
|
||||
Reference string `gorm:"size:255"`
|
||||
SourceLicense string `gorm:"size:32;not null"`
|
||||
Enabled bool `gorm:"not null;default:true;index"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentAIExpertItem) TableName() string {
|
||||
return "senlin_agent_ai_experts"
|
||||
func (SaAIExpertItem) TableName() string {
|
||||
return "sa_ai_experts"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
type SenlinAgentAIKey struct {
|
||||
type SaAIKey struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
UserID uint `gorm:"uniqueIndex;not null"`
|
||||
@@ -13,6 +13,6 @@ type SenlinAgentAIKey struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentAIKey) TableName() string {
|
||||
return "senlin_agent_ai_keys"
|
||||
func (SaAIKey) TableName() string {
|
||||
return "sa_ai_keys"
|
||||
}
|
||||
|
||||
@@ -2,18 +2,18 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
// SenlinAgentAIRateBucket 保存用户在固定窗口内已占用的 AI 请求配额。
|
||||
// SaAIRateBucket 保存用户在固定窗口内已占用的 AI 请求配额。
|
||||
// 复合唯一键让单条 UPSERT 在数据库层完成跨进程并发仲裁。
|
||||
type SenlinAgentAIRateBucket struct {
|
||||
type SaAIRateBucket struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
UserID uint `gorm:"not null;uniqueIndex:uidx_senlin_agent_ai_rate_bucket,priority:1"`
|
||||
Action string `gorm:"size:100;not null;uniqueIndex:uidx_senlin_agent_ai_rate_bucket,priority:2"`
|
||||
WindowStart time.Time `gorm:"not null;uniqueIndex:uidx_senlin_agent_ai_rate_bucket,priority:3"`
|
||||
UserID uint `gorm:"not null;uniqueIndex:uidx_sa_ai_rate_bucket,priority:1"`
|
||||
Action string `gorm:"size:100;not null;uniqueIndex:uidx_sa_ai_rate_bucket,priority:2"`
|
||||
WindowStart time.Time `gorm:"not null;uniqueIndex:uidx_sa_ai_rate_bucket,priority:3"`
|
||||
Count int `gorm:"not null"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentAIRateBucket) TableName() string {
|
||||
return "senlin_agent_ai_rate_buckets"
|
||||
func (SaAIRateBucket) TableName() string {
|
||||
return "sa_ai_rate_buckets"
|
||||
}
|
||||
|
||||
@@ -2,23 +2,23 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
type SenlinAgentAISession struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
ProjectID uint `gorm:"index;not null"`
|
||||
ProjectIdentity string `gorm:"type:char(36);index"`
|
||||
CreatedBy uint `gorm:"index;not null"`
|
||||
CreatedByIdentity string `gorm:"type:char(36);index"`
|
||||
ExpertID *uint `gorm:"index"`
|
||||
ExpertIdentity *string `gorm:"type:char(36);index"`
|
||||
Expert *SenlinAgentAIExpertItem `gorm:"foreignKey:ExpertID"`
|
||||
Title string `gorm:"not null"`
|
||||
Context string `gorm:"type:text"`
|
||||
Status string `gorm:"not null;default:ready"`
|
||||
type SaAISession struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
ProjectID uint `gorm:"index;not null"`
|
||||
ProjectIdentity string `gorm:"type:char(36);index"`
|
||||
CreatedBy uint `gorm:"index;not null"`
|
||||
CreatedByIdentity string `gorm:"type:char(36);index"`
|
||||
ExpertID *uint `gorm:"index"`
|
||||
ExpertIdentity *string `gorm:"type:char(36);index"`
|
||||
Expert *SaAIExpertItem `gorm:"foreignKey:ExpertID"`
|
||||
Title string `gorm:"not null"`
|
||||
Context string `gorm:"type:text"`
|
||||
Status string `gorm:"not null;default:ready"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentAISession) TableName() string {
|
||||
return "senlin_agent_ai_sessions"
|
||||
func (SaAISession) TableName() string {
|
||||
return "sa_ai_sessions"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
type SenlinAgentCronPlan struct {
|
||||
type SaCronPlan struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
ProjectID uint `gorm:"index;not null"`
|
||||
@@ -18,6 +18,6 @@ type SenlinAgentCronPlan struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentCronPlan) TableName() string {
|
||||
return "senlin_agent_cron_plans"
|
||||
func (SaCronPlan) TableName() string {
|
||||
return "sa_cron_plans"
|
||||
}
|
||||
|
||||
@@ -7,162 +7,162 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (m *SenlinAgentUser) BeforeCreate(tx *gorm.DB) error {
|
||||
func (m *SaUser) BeforeCreate(tx *gorm.DB) error {
|
||||
return ensureIdentity(&m.Identity)
|
||||
}
|
||||
|
||||
func (m *SenlinAgentProject) BeforeCreate(tx *gorm.DB) error {
|
||||
func (m *SaProject) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := ensureIdentity(&m.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveIdentity(tx, &SenlinAgentUser{}, m.OwnerID, &m.OwnerIdentity)
|
||||
return resolveIdentity(tx, &SaUser{}, m.OwnerID, &m.OwnerIdentity)
|
||||
}
|
||||
|
||||
func (m *SenlinAgentInboxItem) BeforeCreate(tx *gorm.DB) error {
|
||||
func (m *SaInboxItem) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := ensureIdentity(&m.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
|
||||
if err := resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity)
|
||||
return resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity)
|
||||
}
|
||||
|
||||
func (m *SenlinAgentInboxSuggestion) BeforeCreate(tx *gorm.DB) error {
|
||||
func (m *SaInboxSuggestion) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := ensureIdentity(&m.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveIdentity(tx, &SenlinAgentInboxItem{}, m.InboxItemID, &m.InboxItemIdentity); err != nil {
|
||||
if err := resolveIdentity(tx, &SaInboxItem{}, m.InboxItemID, &m.InboxItemIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity)
|
||||
return resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity)
|
||||
}
|
||||
|
||||
func (m *SenlinAgentTask) BeforeCreate(tx *gorm.DB) error {
|
||||
func (m *SaTask) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := ensureIdentity(&m.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
|
||||
if err := resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
|
||||
if err := resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveOptionalIdentity(tx, &SenlinAgentUser{}, m.AssigneeID, &m.AssigneeIdentity); err != nil {
|
||||
if err := resolveOptionalIdentity(tx, &SaUser{}, m.AssigneeID, &m.AssigneeIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveOptionalIdentity(tx, &SenlinAgentTag{}, m.TagID, &m.TagIdentity); err != nil {
|
||||
if err := resolveOptionalIdentity(tx, &SaTag{}, m.TagID, &m.TagIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveOptionalIdentity(tx, &SenlinAgentInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity)
|
||||
return resolveOptionalIdentity(tx, &SaInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity)
|
||||
}
|
||||
|
||||
func (m *SenlinAgentNote) BeforeCreate(tx *gorm.DB) error {
|
||||
func (m *SaNote) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := ensureIdentity(&m.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
|
||||
if err := resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
|
||||
if err := resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveOptionalIdentity(tx, &SenlinAgentInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity)
|
||||
return resolveOptionalIdentity(tx, &SaInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity)
|
||||
}
|
||||
|
||||
func (m *SenlinAgentSource) BeforeCreate(tx *gorm.DB) error {
|
||||
func (m *SaSource) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := ensureIdentity(&m.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
|
||||
if err := resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
|
||||
if err := resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveOptionalIdentity(tx, &SenlinAgentInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity)
|
||||
return resolveOptionalIdentity(tx, &SaInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity)
|
||||
}
|
||||
|
||||
func (m *SenlinAgentAISession) BeforeCreate(tx *gorm.DB) error {
|
||||
func (m *SaAISession) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := ensureIdentity(&m.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
|
||||
if err := resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
|
||||
if err := resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveOptionalIdentity(tx, &SenlinAgentAIExpertItem{}, m.ExpertID, &m.ExpertIdentity)
|
||||
return resolveOptionalIdentity(tx, &SaAIExpertItem{}, m.ExpertID, &m.ExpertIdentity)
|
||||
}
|
||||
|
||||
func (m *SenlinAgentAIExpertCategory) BeforeCreate(_ *gorm.DB) error {
|
||||
func (m *SaAIExpertCategory) BeforeCreate(_ *gorm.DB) error {
|
||||
return ensureIdentity(&m.Identity)
|
||||
}
|
||||
|
||||
func (m *SenlinAgentAIExpertItem) BeforeCreate(tx *gorm.DB) error {
|
||||
func (m *SaAIExpertItem) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := ensureIdentity(&m.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveIdentity(tx, &SenlinAgentAIExpertCategory{}, m.CategoryID, &m.CategoryIdentity)
|
||||
return resolveIdentity(tx, &SaAIExpertCategory{}, m.CategoryID, &m.CategoryIdentity)
|
||||
}
|
||||
|
||||
func (m *SenlinAgentTag) BeforeCreate(tx *gorm.DB) error {
|
||||
func (m *SaTag) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := ensureIdentity(&m.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity)
|
||||
return resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity)
|
||||
}
|
||||
|
||||
func (m *SenlinAgentProjectChannel) BeforeCreate(tx *gorm.DB) error {
|
||||
func (m *SaProjectChannel) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := ensureIdentity(&m.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity)
|
||||
return resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity)
|
||||
}
|
||||
|
||||
func (m *SenlinAgentCronPlan) BeforeCreate(tx *gorm.DB) error {
|
||||
func (m *SaCronPlan) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := ensureIdentity(&m.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
|
||||
if err := resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveIdentity(tx, &SenlinAgentUser{}, m.CreatedBy, &m.CreatedByIdentity)
|
||||
return resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity)
|
||||
}
|
||||
|
||||
func (m *SenlinAgentProjectEvent) BeforeCreate(tx *gorm.DB) error {
|
||||
func (m *SaProjectEvent) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := ensureIdentity(&m.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveIdentity(tx, &SenlinAgentProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
|
||||
if err := resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveIdentity(tx, &SenlinAgentUser{}, m.ActorID, &m.ActorIdentity); err != nil {
|
||||
if err := resolveIdentity(tx, &SaUser{}, m.ActorID, &m.ActorIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveEntityIdentity(tx, m.EntityType, m.EntityID, &m.EntityIdentity)
|
||||
}
|
||||
|
||||
func (m *SenlinAgentAIKey) BeforeCreate(tx *gorm.DB) error {
|
||||
func (m *SaAIKey) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := ensureIdentity(&m.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveIdentity(tx, &SenlinAgentUser{}, m.UserID, &m.UserIdentity)
|
||||
return resolveIdentity(tx, &SaUser{}, m.UserID, &m.UserIdentity)
|
||||
}
|
||||
|
||||
func (m *SenlinAgentAICallLog) BeforeCreate(tx *gorm.DB) error {
|
||||
func (m *SaAICallLog) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := ensureIdentity(&m.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveIdentity(tx, &SenlinAgentUser{}, m.UserID, &m.UserIdentity)
|
||||
return resolveIdentity(tx, &SaUser{}, m.UserID, &m.UserIdentity)
|
||||
}
|
||||
|
||||
func (m *SenlinAgentTaskShare) BeforeCreate(tx *gorm.DB) error {
|
||||
func (m *SaTaskShare) BeforeCreate(tx *gorm.DB) error {
|
||||
if err := ensureIdentity(&m.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveIdentity(tx, &SenlinAgentTask{}, m.TaskID, &m.TaskIdentity); err != nil {
|
||||
if err := resolveIdentity(tx, &SaTask{}, m.TaskID, &m.TaskIdentity); err != nil {
|
||||
return err
|
||||
}
|
||||
return resolveEntityIdentity(tx, m.ObjectType, m.ObjectID, &m.ObjectIdentity)
|
||||
@@ -210,19 +210,19 @@ func resolveEntityIdentity(tx *gorm.DB, entityType string, entityID uint, identi
|
||||
}
|
||||
switch entityType {
|
||||
case "project":
|
||||
return resolveIdentity(tx, &SenlinAgentProject{}, entityID, identity)
|
||||
return resolveIdentity(tx, &SaProject{}, entityID, identity)
|
||||
case "inbox", "inbox_item":
|
||||
return resolveIdentity(tx, &SenlinAgentInboxItem{}, entityID, identity)
|
||||
return resolveIdentity(tx, &SaInboxItem{}, entityID, identity)
|
||||
case "task":
|
||||
return resolveIdentity(tx, &SenlinAgentTask{}, entityID, identity)
|
||||
return resolveIdentity(tx, &SaTask{}, entityID, identity)
|
||||
case "note":
|
||||
return resolveIdentity(tx, &SenlinAgentNote{}, entityID, identity)
|
||||
return resolveIdentity(tx, &SaNote{}, entityID, identity)
|
||||
case "source":
|
||||
return resolveIdentity(tx, &SenlinAgentSource{}, entityID, identity)
|
||||
return resolveIdentity(tx, &SaSource{}, entityID, identity)
|
||||
case "ai_session":
|
||||
return resolveIdentity(tx, &SenlinAgentAISession{}, entityID, identity)
|
||||
return resolveIdentity(tx, &SaAISession{}, entityID, identity)
|
||||
case "tag":
|
||||
return resolveIdentity(tx, &SenlinAgentTag{}, entityID, identity)
|
||||
return resolveIdentity(tx, &SaTag{}, entityID, identity)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
func TestBeforeCreateAssignsUUIDV7Identity(t *testing.T) {
|
||||
database := newIdentityTestDB(t)
|
||||
user := SenlinAgentUser{Email: "lead@example.com", DisplayName: "Lead", PasswordHash: "hash"}
|
||||
user := SaUser{Email: "lead@example.com", DisplayName: "Lead", PasswordHash: "hash"}
|
||||
|
||||
require.NoError(t, database.Create(&user).Error)
|
||||
|
||||
@@ -21,14 +21,14 @@ func TestBeforeCreateAssignsUUIDV7Identity(t *testing.T) {
|
||||
|
||||
func TestBeforeCreateCopiesParentIdentitiesFromNumericIDs(t *testing.T) {
|
||||
database := newIdentityTestDB(t)
|
||||
user := SenlinAgentUser{Email: "lead@example.com", DisplayName: "Lead", PasswordHash: "hash"}
|
||||
user := SaUser{Email: "lead@example.com", DisplayName: "Lead", PasswordHash: "hash"}
|
||||
require.NoError(t, database.Create(&user).Error)
|
||||
project := SenlinAgentProject{OwnerID: user.ID, Name: "Alpha"}
|
||||
project := SaProject{OwnerID: user.ID, Name: "Alpha"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
item := SenlinAgentInboxItem{ProjectID: project.ID, CreatedBy: user.ID, SourceType: "text"}
|
||||
item := SaInboxItem{ProjectID: project.ID, CreatedBy: user.ID, SourceType: "text"}
|
||||
require.NoError(t, database.Create(&item).Error)
|
||||
|
||||
task := SenlinAgentTask{ProjectID: project.ID, CreatedBy: user.ID, SourceInboxItemID: &item.ID, Title: "Follow up"}
|
||||
task := SaTask{ProjectID: project.ID, CreatedBy: user.ID, SourceInboxItemID: &item.ID, Title: "Follow up"}
|
||||
|
||||
require.NoError(t, database.Create(&task).Error)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
type SenlinAgentInboxItem struct {
|
||||
type SaInboxItem struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
ProjectID uint `gorm:"index;not null"`
|
||||
@@ -18,6 +18,6 @@ type SenlinAgentInboxItem struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentInboxItem) TableName() string {
|
||||
return "senlin_agent_inbox_items"
|
||||
func (SaInboxItem) TableName() string {
|
||||
return "sa_inbox_items"
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
// SenlinAgentInboxSuggestion 保存分析阶段的候选草稿;它不是任务、笔记或资料,只有用户确认后才会创建正式对象。
|
||||
type SenlinAgentInboxSuggestion struct {
|
||||
// SaInboxSuggestion 保存分析阶段的候选草稿;它不是任务、笔记或资料,只有用户确认后才会创建正式对象。
|
||||
type SaInboxSuggestion struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
InboxItemID uint `gorm:"index;not null"`
|
||||
@@ -16,6 +16,6 @@ type SenlinAgentInboxSuggestion struct {
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentInboxSuggestion) TableName() string {
|
||||
return "senlin_agent_inbox_suggestions"
|
||||
func (SaInboxSuggestion) TableName() string {
|
||||
return "sa_inbox_suggestions"
|
||||
}
|
||||
|
||||
48
backend/internal/models/legacy_tables.go
Normal file
48
backend/internal/models/legacy_tables.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var legacyTableRenames = []struct {
|
||||
legacy string
|
||||
current string
|
||||
}{
|
||||
{legacy: "senlin_agent_schema_migrations", current: "sa_schema_migrations"},
|
||||
{legacy: "senlin_agent_users", current: "sa_users"},
|
||||
{legacy: "senlin_agent_projects", current: "sa_projects"},
|
||||
{legacy: "senlin_agent_inbox_items", current: "sa_inbox_items"},
|
||||
{legacy: "senlin_agent_inbox_suggestions", current: "sa_inbox_suggestions"},
|
||||
{legacy: "senlin_agent_tasks", current: "sa_tasks"},
|
||||
{legacy: "senlin_agent_notes", current: "sa_notes"},
|
||||
{legacy: "senlin_agent_sources", current: "sa_sources"},
|
||||
{legacy: "senlin_agent_ai_expert_categories", current: "sa_ai_expert_categories"},
|
||||
{legacy: "senlin_agent_ai_experts", current: "sa_ai_experts"},
|
||||
{legacy: "senlin_agent_ai_sessions", current: "sa_ai_sessions"},
|
||||
{legacy: "senlin_agent_tags", current: "sa_tags"},
|
||||
{legacy: "senlin_agent_project_channels", current: "sa_project_channels"},
|
||||
{legacy: "senlin_agent_cron_plans", current: "sa_cron_plans"},
|
||||
{legacy: "senlin_agent_project_events", current: "sa_project_events"},
|
||||
{legacy: "senlin_agent_ai_keys", current: "sa_ai_keys"},
|
||||
{legacy: "senlin_agent_ai_call_logs", current: "sa_ai_call_logs"},
|
||||
{legacy: "senlin_agent_ai_rate_buckets", current: "sa_ai_rate_buckets"},
|
||||
{legacy: "senlin_agent_task_shares", current: "sa_task_shares"},
|
||||
}
|
||||
|
||||
func renameLegacyTables(database *gorm.DB) error {
|
||||
migrator := database.Migrator()
|
||||
for _, table := range legacyTableRenames {
|
||||
if !migrator.HasTable(table.legacy) {
|
||||
continue
|
||||
}
|
||||
if migrator.HasTable(table.current) {
|
||||
return fmt.Errorf("cannot rename legacy table %s: target table %s already exists", table.legacy, table.current)
|
||||
}
|
||||
if err := migrator.RenameTable(table.legacy, table.current); err != nil {
|
||||
return fmt.Errorf("rename legacy table %s to %s: %w", table.legacy, table.current, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -9,16 +9,16 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SenlinAgentSchemaMigration 记录已完成的数据修复及审计摘要,确保升级可重入且可追溯。
|
||||
type SenlinAgentSchemaMigration struct {
|
||||
// SaSchemaMigration 记录已完成的数据修复及审计摘要,确保升级可重入且可追溯。
|
||||
type SaSchemaMigration struct {
|
||||
Version int `gorm:"primaryKey;autoIncrement:false"`
|
||||
Name string `gorm:"not null"`
|
||||
Details string `gorm:"type:text;not null"`
|
||||
AppliedAt time.Time `gorm:"not null"`
|
||||
}
|
||||
|
||||
func (SenlinAgentSchemaMigration) TableName() string {
|
||||
return "senlin_agent_schema_migrations"
|
||||
func (SaSchemaMigration) TableName() string {
|
||||
return "sa_schema_migrations"
|
||||
}
|
||||
|
||||
type versionedMigration struct {
|
||||
@@ -38,7 +38,7 @@ func runVersionedMigrations(database *gorm.DB) error {
|
||||
// version table are not inherited by the data-repair transaction.
|
||||
migrationDatabase := database.Session(&gorm.Session{NewDB: true})
|
||||
var applied int64
|
||||
if err := migrationDatabase.Model(&SenlinAgentSchemaMigration{}).Where("version = ?", migration.version).Count(&applied).Error; err != nil {
|
||||
if err := migrationDatabase.Model(&SaSchemaMigration{}).Where("version = ?", migration.version).Count(&applied).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if applied > 0 {
|
||||
@@ -53,7 +53,7 @@ func runVersionedMigrations(database *gorm.DB) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&SenlinAgentSchemaMigration{
|
||||
return tx.Create(&SaSchemaMigration{
|
||||
Version: migration.version, Name: migration.name, Details: string(encoded), AppliedAt: time.Now().UTC(),
|
||||
}).Error
|
||||
}); err != nil {
|
||||
@@ -64,8 +64,8 @@ func runVersionedMigrations(database *gorm.DB) error {
|
||||
}
|
||||
|
||||
func normalizeLegacyProjectIdentifiers(tx *gorm.DB) (map[string]int, error) {
|
||||
var projects []SenlinAgentProject
|
||||
if err := tx.Model(&SenlinAgentProject{}).Select("id", "owner_id", "identifier").Order("owner_id asc, id asc").Find(&projects).Error; err != nil {
|
||||
var projects []SaProject
|
||||
if err := tx.Model(&SaProject{}).Select("id", "owner_id", "identifier").Order("owner_id asc, id asc").Find(&projects).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -102,12 +102,12 @@ func normalizeLegacyProjectIdentifiers(tx *gorm.DB) (map[string]int, error) {
|
||||
if candidate == project.Identifier {
|
||||
continue
|
||||
}
|
||||
if err := tx.Model(&SenlinAgentProject{}).Where("id = ?", project.ID).Update("identifier", candidate).Error; err != nil {
|
||||
if err := tx.Model(&SaProject{}).Where("id = ?", project.ID).Update("identifier", candidate).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updated++
|
||||
}
|
||||
if err := tx.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS uidx_senlin_agent_projects_owner_identifier ON senlin_agent_projects (owner_id, identifier)`).Error; err != nil {
|
||||
if err := tx.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS uidx_sa_projects_owner_identifier ON sa_projects (owner_id, identifier)`).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]int{"audited": len(projects), "updated": updated}, nil
|
||||
@@ -124,11 +124,11 @@ func uniqueIdentifier(base string, reserved map[string]struct{}) string {
|
||||
}
|
||||
|
||||
func deduplicateLegacyProjectTags(tx *gorm.DB) (map[string]int, error) {
|
||||
var tags []SenlinAgentTag
|
||||
if err := tx.Model(&SenlinAgentTag{}).Select("id", "identity", "project_id", "name").Order("project_id asc, name asc, id asc").Find(&tags).Error; err != nil {
|
||||
var tags []SaTag
|
||||
if err := tx.Model(&SaTag{}).Select("id", "identity", "project_id", "name").Order("project_id asc, name asc, id asc").Find(&tags).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keepers := make(map[string]SenlinAgentTag)
|
||||
keepers := make(map[string]SaTag)
|
||||
deduplicated := 0
|
||||
for _, tag := range tags {
|
||||
key := fmt.Sprintf("%d\x00%s", tag.ProjectID, tag.Name)
|
||||
@@ -137,17 +137,17 @@ func deduplicateLegacyProjectTags(tx *gorm.DB) (map[string]int, error) {
|
||||
keepers[key] = tag
|
||||
continue
|
||||
}
|
||||
if err := tx.Model(&SenlinAgentTask{}).Where("tag_id = ?", tag.ID).Updates(map[string]any{
|
||||
if err := tx.Model(&SaTask{}).Where("tag_id = ?", tag.ID).Updates(map[string]any{
|
||||
"tag_id": keeper.ID, "tag_identity": keeper.Identity,
|
||||
}).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Where("id = ?", tag.ID).Delete(&SenlinAgentTag{}).Error; err != nil {
|
||||
if err := tx.Where("id = ?", tag.ID).Delete(&SaTag{}).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deduplicated++
|
||||
}
|
||||
if err := tx.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS uidx_senlin_agent_tags_project_name ON senlin_agent_tags (project_id, name)`).Error; err != nil {
|
||||
if err := tx.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS uidx_sa_tags_project_name ON sa_tags (project_id, name)`).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]int{"audited": len(tags), "deduplicated": deduplicated}, nil
|
||||
|
||||
@@ -39,16 +39,22 @@ func TestAutoMigrateUpgradesLegacyProjectsAndTagsWithoutLosingAssociations(t *te
|
||||
// carry an initialized Gorm statement. Exercise that mode so model state from
|
||||
// the migration-version lookup cannot leak into the data-repair transaction.
|
||||
require.NoError(t, AutoMigrate(database.Session(&gorm.Session{Initialized: true})))
|
||||
require.False(t, database.Migrator().HasTable("senlin_agent_projects"))
|
||||
require.False(t, database.Migrator().HasTable("senlin_agent_tags"))
|
||||
require.False(t, database.Migrator().HasTable("senlin_agent_tasks"))
|
||||
require.True(t, database.Migrator().HasTable("sa_projects"))
|
||||
require.True(t, database.Migrator().HasTable("sa_tags"))
|
||||
require.True(t, database.Migrator().HasTable("sa_tasks"))
|
||||
|
||||
var projects []SenlinAgentProject
|
||||
var projects []SaProject
|
||||
require.NoError(t, database.Order("id asc").Find(&projects).Error)
|
||||
require.Len(t, projects, 6, "项目迁移不得删除 legacy 行")
|
||||
require.Equal(t, []string{"project-1", "project-2", "DUP", "DUP-4-2", "DUP-4", "DUP"}, projectIdentifiers(projects))
|
||||
|
||||
var tags []SenlinAgentTag
|
||||
var tags []SaTag
|
||||
require.NoError(t, database.Order("id asc").Find(&tags).Error)
|
||||
require.Equal(t, []uint{10, 12}, tagIDs(tags), "同项目同名标签应保留最早记录,不影响其他项目")
|
||||
var task SenlinAgentTask
|
||||
var task SaTask
|
||||
require.NoError(t, database.First(&task, 20).Error)
|
||||
require.NotNil(t, task.TagID)
|
||||
require.Equal(t, uint(10), *task.TagID)
|
||||
@@ -56,21 +62,32 @@ func TestAutoMigrateUpgradesLegacyProjectsAndTagsWithoutLosingAssociations(t *te
|
||||
require.Equal(t, "tag-keeper", *task.TagIdentity)
|
||||
|
||||
var migrationCount int64
|
||||
require.NoError(t, database.Table("senlin_agent_schema_migrations").Count(&migrationCount).Error)
|
||||
require.NoError(t, database.Table("sa_schema_migrations").Count(&migrationCount).Error)
|
||||
require.Equal(t, int64(2), migrationCount)
|
||||
var auditDetails []string
|
||||
require.NoError(t, database.Table("senlin_agent_schema_migrations").Order("version asc").Pluck("details", &auditDetails).Error)
|
||||
require.NoError(t, database.Table("sa_schema_migrations").Order("version asc").Pluck("details", &auditDetails).Error)
|
||||
require.Contains(t, auditDetails[0], `"updated":3`)
|
||||
require.Contains(t, auditDetails[1], `"deduplicated":1`)
|
||||
require.NoError(t, AutoMigrate(database), "versioned migrations must be safe to run again")
|
||||
require.NoError(t, database.Table("senlin_agent_schema_migrations").Count(&migrationCount).Error)
|
||||
require.NoError(t, database.Table("sa_schema_migrations").Count(&migrationCount).Error)
|
||||
require.Equal(t, int64(2), migrationCount)
|
||||
|
||||
require.Error(t, database.Exec(`INSERT INTO senlin_agent_projects (owner_id, name, identifier) VALUES (7, 'still duplicate', 'DUP')`).Error)
|
||||
require.Error(t, database.Exec(`INSERT INTO senlin_agent_tags (project_id, name) VALUES (1, 'UI')`).Error)
|
||||
require.Error(t, database.Exec(`INSERT INTO sa_projects (owner_id, name, identifier) VALUES (7, 'still duplicate', 'DUP')`).Error)
|
||||
require.Error(t, database.Exec(`INSERT INTO sa_tags (project_id, name) VALUES (1, 'UI')`).Error)
|
||||
}
|
||||
|
||||
func projectIdentifiers(projects []SenlinAgentProject) []string {
|
||||
func TestAutoMigrateRejectsAmbiguousLegacyAndCurrentTables(t *testing.T) {
|
||||
database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, database.Exec(`CREATE TABLE senlin_agent_users (id integer primary key)`).Error)
|
||||
require.NoError(t, database.Exec(`CREATE TABLE sa_users (id integer primary key)`).Error)
|
||||
|
||||
err = AutoMigrate(database)
|
||||
|
||||
require.EqualError(t, err, "cannot rename legacy table senlin_agent_users: target table sa_users already exists")
|
||||
}
|
||||
|
||||
func projectIdentifiers(projects []SaProject) []string {
|
||||
result := make([]string, 0, len(projects))
|
||||
for _, project := range projects {
|
||||
result = append(result, project.Identifier)
|
||||
@@ -78,7 +95,7 @@ func projectIdentifiers(projects []SenlinAgentProject) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
func tagIDs(tags []SenlinAgentTag) []uint {
|
||||
func tagIDs(tags []SaTag) []uint {
|
||||
result := make([]uint, 0, len(tags))
|
||||
for _, tag := range tags {
|
||||
result = append(result, tag.ID)
|
||||
|
||||
@@ -23,28 +23,32 @@ func New(dsn string) error {
|
||||
}
|
||||
|
||||
func AutoMigrate(database *gorm.DB) error {
|
||||
if err := database.AutoMigrate(
|
||||
&SenlinAgentSchemaMigration{},
|
||||
&SenlinAgentUser{},
|
||||
&SenlinAgentProject{},
|
||||
&SenlinAgentInboxItem{},
|
||||
&SenlinAgentInboxSuggestion{},
|
||||
&SenlinAgentTask{},
|
||||
&SenlinAgentNote{},
|
||||
&SenlinAgentSource{},
|
||||
&SenlinAgentAIExpertCategory{},
|
||||
&SenlinAgentAIExpertItem{},
|
||||
&SenlinAgentAISession{},
|
||||
&SenlinAgentTag{},
|
||||
&SenlinAgentProjectChannel{},
|
||||
&SenlinAgentCronPlan{},
|
||||
&SenlinAgentProjectEvent{},
|
||||
&SenlinAgentAIKey{},
|
||||
&SenlinAgentAICallLog{},
|
||||
&SenlinAgentAIRateBucket{},
|
||||
&SenlinAgentTaskShare{},
|
||||
cleanDatabase := database.Session(&gorm.Session{NewDB: true})
|
||||
if err := cleanDatabase.Transaction(renameLegacyTables); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cleanDatabase.AutoMigrate(
|
||||
&SaSchemaMigration{},
|
||||
&SaUser{},
|
||||
&SaProject{},
|
||||
&SaInboxItem{},
|
||||
&SaInboxSuggestion{},
|
||||
&SaTask{},
|
||||
&SaNote{},
|
||||
&SaSource{},
|
||||
&SaAIExpertCategory{},
|
||||
&SaAIExpertItem{},
|
||||
&SaAISession{},
|
||||
&SaTag{},
|
||||
&SaProjectChannel{},
|
||||
&SaCronPlan{},
|
||||
&SaProjectEvent{},
|
||||
&SaAIKey{},
|
||||
&SaAICallLog{},
|
||||
&SaAIRateBucket{},
|
||||
&SaTaskShare{},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return runVersionedMigrations(database)
|
||||
return runVersionedMigrations(cleanDatabase)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
type SenlinAgentNote struct {
|
||||
type SaNote struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
ProjectID uint `gorm:"index;not null"`
|
||||
@@ -17,6 +17,6 @@ type SenlinAgentNote struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentNote) TableName() string {
|
||||
return "senlin_agent_notes"
|
||||
func (SaNote) TableName() string {
|
||||
return "sa_notes"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
type SenlinAgentProject struct {
|
||||
type SaProject struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
OwnerID uint `gorm:"index;not null"`
|
||||
@@ -16,6 +16,6 @@ type SenlinAgentProject struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentProject) TableName() string {
|
||||
return "senlin_agent_projects"
|
||||
func (SaProject) TableName() string {
|
||||
return "sa_projects"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
type SenlinAgentProjectChannel struct {
|
||||
type SaProjectChannel struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
ProjectID uint `gorm:"index;not null"`
|
||||
@@ -15,6 +15,6 @@ type SenlinAgentProjectChannel struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentProjectChannel) TableName() string {
|
||||
return "senlin_agent_project_channels"
|
||||
func (SaProjectChannel) TableName() string {
|
||||
return "sa_project_channels"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
type SenlinAgentProjectEvent struct {
|
||||
type SaProjectEvent struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
ProjectID uint `gorm:"index;not null"`
|
||||
@@ -17,6 +17,6 @@ type SenlinAgentProjectEvent struct {
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentProjectEvent) TableName() string {
|
||||
return "senlin_agent_project_events"
|
||||
func (SaProjectEvent) TableName() string {
|
||||
return "sa_project_events"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
type SenlinAgentSource struct {
|
||||
type SaSource struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
ProjectID uint `gorm:"index;not null"`
|
||||
@@ -20,6 +20,6 @@ type SenlinAgentSource struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentSource) TableName() string {
|
||||
return "senlin_agent_sources"
|
||||
func (SaSource) TableName() string {
|
||||
return "sa_sources"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
type SenlinAgentTag struct {
|
||||
type SaTag struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
ProjectID uint `gorm:"index;not null"`
|
||||
@@ -11,6 +11,6 @@ type SenlinAgentTag struct {
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentTag) TableName() string {
|
||||
return "senlin_agent_tags"
|
||||
func (SaTag) TableName() string {
|
||||
return "sa_tags"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
type SenlinAgentTask struct {
|
||||
type SaTask struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
ProjectID uint `gorm:"index;not null"`
|
||||
@@ -24,6 +24,6 @@ type SenlinAgentTask struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentTask) TableName() string {
|
||||
return "senlin_agent_tasks"
|
||||
func (SaTask) TableName() string {
|
||||
return "sa_tasks"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
type SenlinAgentTaskShare struct {
|
||||
type SaTaskShare struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
TaskID uint `gorm:"index;not null"`
|
||||
@@ -13,6 +13,6 @@ type SenlinAgentTaskShare struct {
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentTaskShare) TableName() string {
|
||||
return "senlin_agent_task_shares"
|
||||
func (SaTaskShare) TableName() string {
|
||||
return "sa_task_shares"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
type SenlinAgentUser struct {
|
||||
type SaUser struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Identity string `gorm:"type:char(36);uniqueIndex"`
|
||||
Email string `gorm:"uniqueIndex;not null"`
|
||||
@@ -13,6 +13,6 @@ type SenlinAgentUser struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (SenlinAgentUser) TableName() string {
|
||||
return "senlin_agent_users"
|
||||
func (SaUser) TableName() string {
|
||||
return "sa_users"
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ type DemoOptions struct {
|
||||
}
|
||||
|
||||
type DemoResult struct {
|
||||
User models.SenlinAgentUser
|
||||
Projects []models.SenlinAgentProject
|
||||
User models.SaUser
|
||||
Projects []models.SaProject
|
||||
}
|
||||
|
||||
func Demo(database *gorm.DB, options DemoOptions) (DemoResult, error) {
|
||||
@@ -53,8 +53,8 @@ func Demo(database *gorm.DB, options DemoOptions) (DemoResult, error) {
|
||||
return result, err
|
||||
}
|
||||
|
||||
func upsertUser(tx *gorm.DB, email string, displayName string, password string) (models.SenlinAgentUser, error) {
|
||||
var user models.SenlinAgentUser
|
||||
func upsertUser(tx *gorm.DB, email string, displayName string, password string) (models.SaUser, error) {
|
||||
var user models.SaUser
|
||||
err := tx.Where("email = ?", email).First(&user).Error
|
||||
if err == nil {
|
||||
return user, nil
|
||||
@@ -66,7 +66,7 @@ func upsertUser(tx *gorm.DB, email string, displayName string, password string)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
user = models.SenlinAgentUser{
|
||||
user = models.SaUser{
|
||||
Email: email,
|
||||
DisplayName: displayName,
|
||||
PasswordHash: string(hash),
|
||||
@@ -75,7 +75,7 @@ func upsertUser(tx *gorm.DB, email string, displayName string, password string)
|
||||
return user, tx.Create(&user).Error
|
||||
}
|
||||
|
||||
func seedProjects(tx *gorm.DB, ownerID uint) ([]models.SenlinAgentProject, error) {
|
||||
func seedProjects(tx *gorm.DB, ownerID uint) ([]models.SaProject, error) {
|
||||
definitions := []struct {
|
||||
Name string
|
||||
Identifier string
|
||||
@@ -87,7 +87,7 @@ func seedProjects(tx *gorm.DB, ownerID uint) ([]models.SenlinAgentProject, error
|
||||
{Name: "运营自动化", Identifier: "AUTO", Description: "定时任务和业务流程自动化"},
|
||||
}
|
||||
|
||||
projects := make([]models.SenlinAgentProject, 0, len(definitions))
|
||||
projects := make([]models.SaProject, 0, len(definitions))
|
||||
for _, definition := range definitions {
|
||||
project, err := firstOrCreateProject(tx, ownerID, definition.Name, definition.Identifier, definition.Description)
|
||||
if err != nil {
|
||||
@@ -109,8 +109,8 @@ func seedProjects(tx *gorm.DB, ownerID uint) ([]models.SenlinAgentProject, error
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
func firstOrCreateProject(tx *gorm.DB, ownerID uint, name string, identifier string, description string) (models.SenlinAgentProject, error) {
|
||||
var project models.SenlinAgentProject
|
||||
func firstOrCreateProject(tx *gorm.DB, ownerID uint, name string, identifier string, description string) (models.SaProject, error) {
|
||||
var project models.SaProject
|
||||
err := tx.Where("owner_id = ? AND name = ?", ownerID, name).First(&project).Error
|
||||
if err == nil {
|
||||
return project, nil
|
||||
@@ -118,7 +118,7 @@ func firstOrCreateProject(tx *gorm.DB, ownerID uint, name string, identifier str
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return project, err
|
||||
}
|
||||
project = models.SenlinAgentProject{OwnerID: ownerID, Name: name, Identifier: identifier, Description: description}
|
||||
project = models.SaProject{OwnerID: ownerID, Name: name, Identifier: identifier, Description: description}
|
||||
return project, tx.Create(&project).Error
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ func seedProjectA1(tx *gorm.DB, userID uint, projectID uint) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range []models.SenlinAgentInboxItem{
|
||||
for _, item := range []models.SaInboxItem{
|
||||
{ProjectID: projectID, CreatedBy: userID, SourceType: "产品研究", Title: "竞争对手定价更新监控", Body: "跟进企业版价格调整和功能组合变化。", Status: "open"},
|
||||
{ProjectID: projectID, CreatedBy: userID, SourceType: "客户支持", Title: "客户反馈:导出功能报错", Body: "客户在导出智能报表时遇到权限校验异常。", Status: "open"},
|
||||
{ProjectID: projectID, CreatedBy: userID, SourceType: "架构讨论", Title: "API 调用策略评估建议", Body: "评估批量任务调度和 AI 限流策略。", Status: "open"},
|
||||
@@ -155,7 +155,7 @@ func seedProjectA1(tx *gorm.DB, userID uint, projectID uint) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, task := range []models.SenlinAgentTask{
|
||||
for _, task := range []models.SaTask{
|
||||
{ProjectID: projectID, CreatedBy: userID, TagID: &uiTagID, Title: "智能报表导出功能", Description: "修复权限校验并补充导出失败提示。", Status: "open", DueAt: &dueSoon, SortOrder: 1},
|
||||
{ProjectID: projectID, CreatedBy: userID, TagID: &automationTagID, Title: "企业版权限体系梳理", Description: "整理角色、项目和任务分享边界。", Status: "open", DueAt: &dueLater, SortOrder: 2},
|
||||
{ProjectID: projectID, CreatedBy: userID, TagID: &feedbackTagID, Title: "Q3 营销策略制定", Description: "汇总竞品情报和客户反馈,生成策略草案。", Status: "open", DueAt: &dueLater, SortOrder: 3},
|
||||
@@ -164,7 +164,7 @@ func seedProjectA1(tx *gorm.DB, userID uint, projectID uint) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, session := range []models.SenlinAgentAISession{
|
||||
for _, session := range []models.SaAISession{
|
||||
{ProjectID: projectID, CreatedBy: userID, Title: "生成 Q3 营销策略初稿", Context: "基于市场分析和竞品数据,输出 Q3 营销策略重点。"},
|
||||
{ProjectID: projectID, CreatedBy: userID, Title: "产品定价模型讨论", Context: "对企业版与标准版定价模型进行对比分析。"},
|
||||
{ProjectID: projectID, CreatedBy: userID, Title: "客户流失原因分析", Context: "分析近三个月客户流失数据与反馈原因。"},
|
||||
@@ -174,7 +174,7 @@ func seedProjectA1(tx *gorm.DB, userID uint, projectID uint) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, note := range []models.SenlinAgentNote{
|
||||
for _, note := range []models.SaNote{
|
||||
{ProjectID: projectID, CreatedBy: userID, Title: "用户权限体系设计文档 v2.1", Markdown: "# 权限体系\n\n围绕项目、任务和资料分享做保守授权。"},
|
||||
{ProjectID: projectID, CreatedBy: userID, Title: "导出功能技术方案评审", Markdown: "# 导出功能\n\n记录权限校验和任务队列方案。"},
|
||||
} {
|
||||
@@ -182,7 +182,7 @@ func seedProjectA1(tx *gorm.DB, userID uint, projectID uint) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, source := range []models.SenlinAgentSource{
|
||||
for _, source := range []models.SaSource{
|
||||
{ProjectID: projectID, CreatedBy: userID, Kind: "file", Title: "2026 Q3 竞品功能对比表", FilePath: "projects/a1/competitors.xlsx", ContentText: "竞品功能矩阵和价格摘要"},
|
||||
{ProjectID: projectID, CreatedBy: userID, Kind: "link", Title: "客户访谈记录索引", URL: "https://example.com/interviews", ContentText: "客户访谈和问题归类"},
|
||||
} {
|
||||
@@ -190,7 +190,7 @@ func seedProjectA1(tx *gorm.DB, userID uint, projectID uint) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, plan := range []models.SenlinAgentCronPlan{
|
||||
for _, plan := range []models.SaCronPlan{
|
||||
{ProjectID: projectID, CreatedBy: userID, Title: "每日 Inbox 摘要", Schedule: "0 9 * * *", NextRunAt: &nextRun, Enabled: true},
|
||||
{ProjectID: projectID, CreatedBy: userID, Title: "每周任务风险扫描", Schedule: "0 10 * * 1", NextRunAt: &dueSoon, Enabled: true},
|
||||
{ProjectID: projectID, CreatedBy: userID, Title: "月底知识归档提醒", Schedule: "0 18 28 * *", NextRunAt: &dueLater, Enabled: false},
|
||||
@@ -199,7 +199,7 @@ func seedProjectA1(tx *gorm.DB, userID uint, projectID uint) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, channel := range []models.SenlinAgentProjectChannel{
|
||||
for _, channel := range []models.SaProjectChannel{
|
||||
{ProjectID: projectID, Title: "客户研究频道", Icon: "user", URL: "https://example.com/research", SortOrder: 7},
|
||||
{ProjectID: projectID, Title: "产品设计频道", Icon: "link", URL: "https://example.com/design", SortOrder: 8},
|
||||
} {
|
||||
@@ -211,39 +211,39 @@ func seedProjectA1(tx *gorm.DB, userID uint, projectID uint) error {
|
||||
}
|
||||
|
||||
func seedCompactProject(tx *gorm.DB, userID uint, projectID uint) error {
|
||||
item := models.SenlinAgentInboxItem{ProjectID: projectID, CreatedBy: userID, SourceType: "手动收集", Title: "待处理事项", Body: "项目启动资料待整理。", Status: "open"}
|
||||
item := models.SaInboxItem{ProjectID: projectID, CreatedBy: userID, SourceType: "手动收集", Title: "待处理事项", Body: "项目启动资料待整理。", Status: "open"}
|
||||
if err := firstOrCreateInbox(tx, item); err != nil {
|
||||
return err
|
||||
}
|
||||
task := models.SenlinAgentTask{ProjectID: projectID, CreatedBy: userID, Title: "项目例会纪要整理", Description: "归档本周同步内容。", Status: "open"}
|
||||
task := models.SaTask{ProjectID: projectID, CreatedBy: userID, Title: "项目例会纪要整理", Description: "归档本周同步内容。", Status: "open"}
|
||||
return firstOrCreateTask(tx, task)
|
||||
}
|
||||
|
||||
func firstOrCreateTag(tx *gorm.DB, projectID uint, name string) error {
|
||||
candidate := models.SenlinAgentTag{ProjectID: projectID, Name: name}
|
||||
candidate := models.SaTag{ProjectID: projectID, Name: name}
|
||||
if err := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "project_id"}, {Name: "name"}},
|
||||
DoNothing: true,
|
||||
}).Create(&candidate).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Where("project_id = ? AND name = ?", projectID, name).First(&models.SenlinAgentTag{}).Error
|
||||
return tx.Where("project_id = ? AND name = ?", projectID, name).First(&models.SaTag{}).Error
|
||||
}
|
||||
|
||||
func tagID(tx *gorm.DB, projectID uint, name string) (uint, error) {
|
||||
var tag models.SenlinAgentTag
|
||||
var tag models.SaTag
|
||||
if err := tx.Where("project_id = ? AND name = ?", projectID, name).First(&tag).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return tag.ID, nil
|
||||
}
|
||||
|
||||
func firstOrCreateInbox(tx *gorm.DB, item models.SenlinAgentInboxItem) error {
|
||||
return firstOrCreate(tx, &models.SenlinAgentInboxItem{}, "project_id = ? AND title = ?", []any{item.ProjectID, item.Title}, item)
|
||||
func firstOrCreateInbox(tx *gorm.DB, item models.SaInboxItem) error {
|
||||
return firstOrCreate(tx, &models.SaInboxItem{}, "project_id = ? AND title = ?", []any{item.ProjectID, item.Title}, item)
|
||||
}
|
||||
|
||||
func firstOrCreateTask(tx *gorm.DB, task models.SenlinAgentTask) error {
|
||||
var existing models.SenlinAgentTask
|
||||
func firstOrCreateTask(tx *gorm.DB, task models.SaTask) error {
|
||||
var existing models.SaTask
|
||||
err := tx.Where("project_id = ? AND title = ?", task.ProjectID, task.Title).First(&existing).Error
|
||||
if err == nil {
|
||||
if task.TagID != nil && existing.TagID == nil {
|
||||
@@ -257,24 +257,24 @@ func firstOrCreateTask(tx *gorm.DB, task models.SenlinAgentTask) error {
|
||||
return tx.Create(&task).Error
|
||||
}
|
||||
|
||||
func firstOrCreateAISession(tx *gorm.DB, session models.SenlinAgentAISession) error {
|
||||
return firstOrCreate(tx, &models.SenlinAgentAISession{}, "project_id = ? AND title = ?", []any{session.ProjectID, session.Title}, session)
|
||||
func firstOrCreateAISession(tx *gorm.DB, session models.SaAISession) error {
|
||||
return firstOrCreate(tx, &models.SaAISession{}, "project_id = ? AND title = ?", []any{session.ProjectID, session.Title}, session)
|
||||
}
|
||||
|
||||
func firstOrCreateNote(tx *gorm.DB, note models.SenlinAgentNote) error {
|
||||
return firstOrCreate(tx, &models.SenlinAgentNote{}, "project_id = ? AND title = ?", []any{note.ProjectID, note.Title}, note)
|
||||
func firstOrCreateNote(tx *gorm.DB, note models.SaNote) error {
|
||||
return firstOrCreate(tx, &models.SaNote{}, "project_id = ? AND title = ?", []any{note.ProjectID, note.Title}, note)
|
||||
}
|
||||
|
||||
func firstOrCreateSource(tx *gorm.DB, source models.SenlinAgentSource) error {
|
||||
return firstOrCreate(tx, &models.SenlinAgentSource{}, "project_id = ? AND title = ?", []any{source.ProjectID, source.Title}, source)
|
||||
func firstOrCreateSource(tx *gorm.DB, source models.SaSource) error {
|
||||
return firstOrCreate(tx, &models.SaSource{}, "project_id = ? AND title = ?", []any{source.ProjectID, source.Title}, source)
|
||||
}
|
||||
|
||||
func firstOrCreateCronPlan(tx *gorm.DB, plan models.SenlinAgentCronPlan) error {
|
||||
return firstOrCreate(tx, &models.SenlinAgentCronPlan{}, "project_id = ? AND title = ?", []any{plan.ProjectID, plan.Title}, plan)
|
||||
func firstOrCreateCronPlan(tx *gorm.DB, plan models.SaCronPlan) error {
|
||||
return firstOrCreate(tx, &models.SaCronPlan{}, "project_id = ? AND title = ?", []any{plan.ProjectID, plan.Title}, plan)
|
||||
}
|
||||
|
||||
func firstOrCreateChannel(tx *gorm.DB, channel models.SenlinAgentProjectChannel) error {
|
||||
return firstOrCreate(tx, &models.SenlinAgentProjectChannel{}, "project_id = ? AND title = ?", []any{channel.ProjectID, channel.Title}, channel)
|
||||
func firstOrCreateChannel(tx *gorm.DB, channel models.SaProjectChannel) error {
|
||||
return firstOrCreate(tx, &models.SaProjectChannel{}, "project_id = ? AND title = ?", []any{channel.ProjectID, channel.Title}, channel)
|
||||
}
|
||||
|
||||
func firstOrCreate[T any](tx *gorm.DB, model *T, where string, args []any, create T) error {
|
||||
|
||||
@@ -41,9 +41,9 @@ func TestPostgresDemoSeedSerializesConcurrentRunsForSameOwner(t *testing.T) {
|
||||
email := "postgres-seed-" + suffix + "@example.com"
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte("original-password"), bcrypt.MinCost)
|
||||
require.NoError(t, err)
|
||||
user := models.SenlinAgentUser{Email: email, DisplayName: "保留用户名称", PasswordHash: string(passwordHash), Role: "user"}
|
||||
user := models.SaUser{Email: email, DisplayName: "保留用户名称", PasswordHash: string(passwordHash), Role: "user"}
|
||||
require.NoError(t, database.Create(&user).Error)
|
||||
projects := []models.SenlinAgentProject{
|
||||
projects := []models.SaProject{
|
||||
{OwnerID: user.ID, Name: "项目 A1", Identifier: "A1"},
|
||||
{OwnerID: user.ID, Name: "项目 A2", Identifier: "A2"},
|
||||
{OwnerID: user.ID, Name: "数据中台", Identifier: "DATA"},
|
||||
@@ -52,9 +52,9 @@ func TestPostgresDemoSeedSerializesConcurrentRunsForSameOwner(t *testing.T) {
|
||||
for index := range projects {
|
||||
require.NoError(t, database.Create(&projects[index]).Error)
|
||||
}
|
||||
uiTag := models.SenlinAgentTag{ProjectID: projects[0].ID, Name: "UI"}
|
||||
uiTag := models.SaTag{ProjectID: projects[0].ID, Name: "UI"}
|
||||
require.NoError(t, database.Create(&uiTag).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentTask{
|
||||
require.NoError(t, database.Create(&models.SaTask{
|
||||
ProjectID: projects[0].ID, CreatedBy: user.ID, TagID: &uiTag.ID,
|
||||
Title: "智能报表导出功能", Status: "open",
|
||||
}).Error)
|
||||
@@ -76,7 +76,7 @@ func TestPostgresDemoSeedSerializesConcurrentRunsForSameOwner(t *testing.T) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if tx.Statement.Schema.Table == (models.SenlinAgentUser{}).TableName() {
|
||||
if tx.Statement.Schema.Table == (models.SaUser{}).TableName() {
|
||||
if _, locking := tx.Statement.Clauses["FOR"]; locking && marker == secondDemoSeedRun {
|
||||
signalDemoSeedStage(secondOwnerQueryAttempted)
|
||||
}
|
||||
@@ -135,22 +135,22 @@ func TestPostgresDemoSeedSerializesConcurrentRunsForSameOwner(t *testing.T) {
|
||||
require.NoError(t, waitForDemoSeedResult(t, ctx, firstResult, "first Demo result"))
|
||||
require.NoError(t, waitForDemoSeedResult(t, ctx, secondResult, "second Demo result"))
|
||||
|
||||
var storedUser models.SenlinAgentUser
|
||||
var storedUser models.SaUser
|
||||
require.NoError(t, database.Where("email = ?", email).First(&storedUser).Error)
|
||||
require.Equal(t, "保留用户名称", storedUser.DisplayName)
|
||||
require.NoError(t, bcrypt.CompareHashAndPassword([]byte(storedUser.PasswordHash), []byte("original-password")))
|
||||
require.Error(t, bcrypt.CompareHashAndPassword([]byte(storedUser.PasswordHash), []byte("new-password")))
|
||||
|
||||
projectIDs := []uint{projects[0].ID, projects[1].ID, projects[2].ID, projects[3].ID}
|
||||
assertPostgresDemoCount(t, database, &models.SenlinAgentProject{}, "owner_id = ?", []any{user.ID}, 4)
|
||||
assertPostgresDemoCount(t, database, &models.SenlinAgentTag{}, "project_id = ?", []any{projects[0].ID}, 4)
|
||||
assertPostgresDemoCount(t, database, &models.SenlinAgentTask{}, "project_id IN ?", []any{projectIDs}, 6)
|
||||
assertPostgresDemoCount(t, database, &models.SenlinAgentInboxItem{}, "project_id IN ?", []any{projectIDs}, 7)
|
||||
assertPostgresDemoCount(t, database, &models.SenlinAgentAISession{}, "project_id = ?", []any{projects[0].ID}, 4)
|
||||
assertPostgresDemoCount(t, database, &models.SenlinAgentNote{}, "project_id = ?", []any{projects[0].ID}, 2)
|
||||
assertPostgresDemoCount(t, database, &models.SenlinAgentSource{}, "project_id = ?", []any{projects[0].ID}, 2)
|
||||
assertPostgresDemoCount(t, database, &models.SenlinAgentCronPlan{}, "project_id = ?", []any{projects[0].ID}, 3)
|
||||
assertPostgresDemoCount(t, database, &models.SenlinAgentProjectChannel{}, "project_id = ?", []any{projects[0].ID}, 2)
|
||||
assertPostgresDemoCount(t, database, &models.SaProject{}, "owner_id = ?", []any{user.ID}, 4)
|
||||
assertPostgresDemoCount(t, database, &models.SaTag{}, "project_id = ?", []any{projects[0].ID}, 4)
|
||||
assertPostgresDemoCount(t, database, &models.SaTask{}, "project_id IN ?", []any{projectIDs}, 6)
|
||||
assertPostgresDemoCount(t, database, &models.SaInboxItem{}, "project_id IN ?", []any{projectIDs}, 7)
|
||||
assertPostgresDemoCount(t, database, &models.SaAISession{}, "project_id = ?", []any{projects[0].ID}, 4)
|
||||
assertPostgresDemoCount(t, database, &models.SaNote{}, "project_id = ?", []any{projects[0].ID}, 2)
|
||||
assertPostgresDemoCount(t, database, &models.SaSource{}, "project_id = ?", []any{projects[0].ID}, 2)
|
||||
assertPostgresDemoCount(t, database, &models.SaCronPlan{}, "project_id = ?", []any{projects[0].ID}, 3)
|
||||
assertPostgresDemoCount(t, database, &models.SaProjectChannel{}, "project_id = ?", []any{projects[0].ID}, 2)
|
||||
}
|
||||
|
||||
func signalDemoSeedStage(stage chan<- struct{}) {
|
||||
@@ -188,7 +188,7 @@ func assertPostgresDemoCount(t *testing.T, database *gorm.DB, model any, where s
|
||||
}
|
||||
|
||||
func cleanupPostgresDemoSeed(database *gorm.DB, userID uint) {
|
||||
var projects []models.SenlinAgentProject
|
||||
var projects []models.SaProject
|
||||
if database.Where("owner_id = ?", userID).Find(&projects).Error != nil {
|
||||
return
|
||||
}
|
||||
@@ -197,34 +197,34 @@ func cleanupPostgresDemoSeed(database *gorm.DB, userID uint) {
|
||||
projectIDs = append(projectIDs, project.ID)
|
||||
}
|
||||
if len(projectIDs) != 0 {
|
||||
var tasks []models.SenlinAgentTask
|
||||
var tasks []models.SaTask
|
||||
database.Where("project_id IN ?", projectIDs).Find(&tasks)
|
||||
taskIDs := make([]uint, 0, len(tasks))
|
||||
for _, task := range tasks {
|
||||
taskIDs = append(taskIDs, task.ID)
|
||||
}
|
||||
if len(taskIDs) != 0 {
|
||||
database.Where("task_id IN ?", taskIDs).Delete(&models.SenlinAgentTaskShare{})
|
||||
database.Where("task_id IN ?", taskIDs).Delete(&models.SaTaskShare{})
|
||||
}
|
||||
var inboxItems []models.SenlinAgentInboxItem
|
||||
var inboxItems []models.SaInboxItem
|
||||
database.Where("project_id IN ?", projectIDs).Find(&inboxItems)
|
||||
inboxIDs := make([]uint, 0, len(inboxItems))
|
||||
for _, item := range inboxItems {
|
||||
inboxIDs = append(inboxIDs, item.ID)
|
||||
}
|
||||
if len(inboxIDs) != 0 {
|
||||
database.Where("inbox_item_id IN ?", inboxIDs).Delete(&models.SenlinAgentInboxSuggestion{})
|
||||
database.Where("inbox_item_id IN ?", inboxIDs).Delete(&models.SaInboxSuggestion{})
|
||||
}
|
||||
database.Where("project_id IN ?", projectIDs).Delete(&models.SenlinAgentProjectEvent{})
|
||||
database.Where("project_id IN ?", projectIDs).Delete(&models.SenlinAgentTask{})
|
||||
database.Where("project_id IN ?", projectIDs).Delete(&models.SenlinAgentNote{})
|
||||
database.Where("project_id IN ?", projectIDs).Delete(&models.SenlinAgentSource{})
|
||||
database.Where("project_id IN ?", projectIDs).Delete(&models.SenlinAgentAISession{})
|
||||
database.Where("project_id IN ?", projectIDs).Delete(&models.SenlinAgentTag{})
|
||||
database.Where("project_id IN ?", projectIDs).Delete(&models.SenlinAgentProjectChannel{})
|
||||
database.Where("project_id IN ?", projectIDs).Delete(&models.SenlinAgentCronPlan{})
|
||||
database.Where("project_id IN ?", projectIDs).Delete(&models.SenlinAgentInboxItem{})
|
||||
database.Where("id IN ?", projectIDs).Delete(&models.SenlinAgentProject{})
|
||||
database.Where("project_id IN ?", projectIDs).Delete(&models.SaProjectEvent{})
|
||||
database.Where("project_id IN ?", projectIDs).Delete(&models.SaTask{})
|
||||
database.Where("project_id IN ?", projectIDs).Delete(&models.SaNote{})
|
||||
database.Where("project_id IN ?", projectIDs).Delete(&models.SaSource{})
|
||||
database.Where("project_id IN ?", projectIDs).Delete(&models.SaAISession{})
|
||||
database.Where("project_id IN ?", projectIDs).Delete(&models.SaTag{})
|
||||
database.Where("project_id IN ?", projectIDs).Delete(&models.SaProjectChannel{})
|
||||
database.Where("project_id IN ?", projectIDs).Delete(&models.SaCronPlan{})
|
||||
database.Where("project_id IN ?", projectIDs).Delete(&models.SaInboxItem{})
|
||||
database.Where("id IN ?", projectIDs).Delete(&models.SaProject{})
|
||||
}
|
||||
database.Delete(&models.SenlinAgentUser{}, userID)
|
||||
database.Delete(&models.SaUser{}, userID)
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestDemoSeedIsIdempotent(t *testing.T) {
|
||||
require.Equal(t, first.User.ID, second.User.ID)
|
||||
require.Len(t, second.Projects, 4)
|
||||
var userCount int64
|
||||
require.NoError(t, database.Model(&models.SenlinAgentUser{}).Where("email = ?", "demo@senlin.ai").Count(&userCount).Error)
|
||||
require.NoError(t, database.Model(&models.SaUser{}).Where("email = ?", "demo@senlin.ai").Count(&userCount).Error)
|
||||
require.Equal(t, int64(1), userCount)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
CREATE INDEX IF NOT EXISTS idx_senlin_agent_projects_search
|
||||
ON senlin_agent_projects
|
||||
CREATE INDEX IF NOT EXISTS idx_sa_projects_search
|
||||
ON sa_projects
|
||||
USING gin (to_tsvector('simple', coalesce(name, '') || ' ' || coalesce(description, '')));
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_senlin_agent_notes_search
|
||||
ON senlin_agent_notes
|
||||
CREATE INDEX IF NOT EXISTS idx_sa_notes_search
|
||||
ON sa_notes
|
||||
USING gin (to_tsvector('simple', coalesce(title, '') || ' ' || coalesce(markdown, '')));
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_senlin_agent_sources_search
|
||||
ON senlin_agent_sources
|
||||
CREATE INDEX IF NOT EXISTS idx_sa_sources_search
|
||||
ON sa_sources
|
||||
USING gin (to_tsvector('simple', coalesce(title, '') || ' ' || coalesce(url, '') || ' ' || coalesce(content_text, '')));
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_senlin_agent_inbox_items_search
|
||||
ON senlin_agent_inbox_items
|
||||
CREATE INDEX IF NOT EXISTS idx_sa_inbox_items_search
|
||||
ON sa_inbox_items
|
||||
USING gin (to_tsvector('simple', coalesce(title, '') || ' ' || coalesce(body, '')));
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_senlin_agent_tasks_search
|
||||
ON senlin_agent_tasks
|
||||
CREATE INDEX IF NOT EXISTS idx_sa_tasks_search
|
||||
ON sa_tasks
|
||||
USING gin (to_tsvector('simple', coalesce(title, '') || ' ' || coalesce(description, '')));
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_senlin_agent_ai_sessions_search
|
||||
ON senlin_agent_ai_sessions
|
||||
CREATE INDEX IF NOT EXISTS idx_sa_ai_sessions_search
|
||||
ON sa_ai_sessions
|
||||
USING gin (to_tsvector('simple', coalesce(title, '') || ' ' || coalesce(context, '')));
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
## Global Constraints
|
||||
|
||||
- 外部 API 前缀为 `/api/v1`,字段为 camelCase,对外 ID 为 UUIDv7 identity。
|
||||
- 数据库模型和表名前缀规则保持 `SenlinAgent` / `senlin_agent_`。
|
||||
- 数据库模型和表名前缀规则保持 `Sa` / `sa_`。
|
||||
- session token 必须签名并过期;handler 只从认证 middleware 读取用户。
|
||||
- 文件路径必须由文件服务构造;标签必须限定在项目内。
|
||||
- AI 请求必须限流并记录 provider、key 类型、action、status 和 error。
|
||||
@@ -121,7 +121,7 @@ type ProjectDTO struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func projectDTO(project models.SenlinAgentProject) ProjectDTO {
|
||||
func projectDTO(project models.SaProject) ProjectDTO {
|
||||
return ProjectDTO{ID: project.Identity, Name: project.Name, Identifier: project.Identifier, Icon: project.Icon, Background: project.Background, Description: project.Description}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -240,7 +240,7 @@ AI provider 请求必须先经过限流和密钥选择,随后记录 provider
|
||||
|
||||
## 8. 数据与配置
|
||||
|
||||
- 保留 `senlin_agent_` 表名前缀和 `SenlinAgent` 模型前缀。
|
||||
- 保留 `sa_` 表名前缀和 `Sa` 模型前缀。
|
||||
- 本地开发数据允许清空后通过 `go run ./cmd/seed` 重建。
|
||||
- 代码不得在启动时自动 drop 表或删除生产数据。
|
||||
- Docker Compose、`agent.dev.yaml` 和 README 使用一致的 PostgreSQL 用户、密码、数据库名和端口。
|
||||
|
||||
Reference in New Issue
Block a user