diff --git a/AGENTS.md b/AGENTS.md index 3975391..326251e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 阶段不要引入全局标签体系。 diff --git a/backend/internal/initdb/expert.go b/backend/internal/initdb/expert.go index 16fb605..e78e19d 100644 --- a/backend/internal/initdb/expert.go +++ b/backend/internal/initdb/expert.go @@ -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, diff --git a/backend/internal/initdb/expert_test.go b/backend/internal/initdb/expert_test.go index a988a55..b163cef 100644 --- a/backend/internal/initdb/expert_test.go +++ b/backend/internal/initdb/expert_test.go @@ -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) } diff --git a/backend/internal/initdb/user.go b/backend/internal/initdb/user.go index b3ddd38..02648d4 100644 --- a/backend/internal/initdb/user.go +++ b/backend/internal/initdb/user.go @@ -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), diff --git a/backend/internal/initdb/user_test.go b/backend/internal/initdb/user_test.go index 5a2b12a..c5a7541 100644 --- a/backend/internal/initdb/user_test.go +++ b/backend/internal/initdb/user_test.go @@ -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 } diff --git a/backend/internal/logic/ai/experts.go b/backend/internal/logic/ai/experts.go index 2438a28..f534e11 100644 --- a/backend/internal/logic/ai/experts.go +++ b/backend/internal/logic/ai/experts.go @@ -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 diff --git a/backend/internal/logic/ai/gateway.go b/backend/internal/logic/ai/gateway.go index 9fb43c2..3aedada 100644 --- a/backend/internal/logic/ai/gateway.go +++ b/backend/internal/logic/ai/gateway.go @@ -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 { diff --git a/backend/internal/logic/ai/gateway_test.go b/backend/internal/logic/ai/gateway_test.go index 0e88694..81c082c 100644 --- a/backend/internal/logic/ai/gateway_test.go +++ b/backend/internal/logic/ai/gateway_test.go @@ -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")) diff --git a/backend/internal/logic/ai/handlers.go b/backend/internal/logic/ai/handlers.go index ef59833..9ec6be7 100644 --- a/backend/internal/logic/ai/handlers.go +++ b/backend/internal/logic/ai/handlers.go @@ -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, diff --git a/backend/internal/logic/ai/handlers_test.go b/backend/internal/logic/ai/handlers_test.go index 93b0abc..0353e23 100644 --- a/backend/internal/logic/ai/handlers_test.go +++ b/backend/internal/logic/ai/handlers_test.go @@ -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 } diff --git a/backend/internal/logic/ai/rate_limit_postgres_test.go b/backend/internal/logic/ai/rate_limit_postgres_test.go index 55e589e..bb8b64f 100644 --- a/backend/internal/logic/ai/rate_limit_postgres_test.go +++ b/backend/internal/logic/ai/rate_limit_postgres_test.go @@ -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) } diff --git a/backend/internal/logic/ai/rate_limit_test.go b/backend/internal/logic/ai/rate_limit_test.go index 94db4b0..5bef1bb 100644 --- a/backend/internal/logic/ai/rate_limit_test.go +++ b/backend/internal/logic/ai/rate_limit_test.go @@ -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 } diff --git a/backend/internal/logic/ai/sessions.go b/backend/internal/logic/ai/sessions.go index c5feead..867ee63 100644 --- a/backend/internal/logic/ai/sessions.go +++ b/backend/internal/logic/ai/sessions.go @@ -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 } diff --git a/backend/internal/logic/auth/handlers_test.go b/backend/internal/logic/auth/handlers_test.go index f811f2a..a237127 100644 --- a/backend/internal/logic/auth/handlers_test.go +++ b/backend/internal/logic/auth/handlers_test.go @@ -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"}) diff --git a/backend/internal/logic/auth/service.go b/backend/internal/logic/auth/service.go index eff5cb3..469651e 100644 --- a/backend/internal/logic/auth/service.go +++ b/backend/internal/logic/auth/service.go @@ -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") } diff --git a/backend/internal/logic/files/handlers.go b/backend/internal/logic/files/handlers.go index 45cf15b..702a504 100644 --- a/backend/internal/logic/files/handlers.go +++ b/backend/internal/logic/files/handlers.go @@ -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)), diff --git a/backend/internal/logic/files/handlers_test.go b/backend/internal/logic/files/handlers_test.go index 10227a5..4f62705 100644 --- a/backend/internal/logic/files/handlers_test.go +++ b/backend/internal/logic/files/handlers_test.go @@ -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) diff --git a/backend/internal/logic/files/service.go b/backend/internal/logic/files/service.go index c7d3aba..3fce8fc 100644 --- a/backend/internal/logic/files/service.go +++ b/backend/internal/logic/files/service.go @@ -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, } diff --git a/backend/internal/logic/inbox/handlers.go b/backend/internal/logic/inbox/handlers.go index 80d0d4d..7eaa6ae 100644 --- a/backend/internal/logic/inbox/handlers.go +++ b/backend/internal/logic/inbox/handlers.go @@ -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, diff --git a/backend/internal/logic/inbox/service.go b/backend/internal/logic/inbox/service.go index 7e3d777..50403da 100644 --- a/backend/internal/logic/inbox/service.go +++ b/backend/internal/logic/inbox/service.go @@ -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 diff --git a/backend/internal/logic/inbox/service_test.go b/backend/internal/logic/inbox/service_test.go index 7220d22..c6552e7 100644 --- a/backend/internal/logic/inbox/service_test.go +++ b/backend/internal/logic/inbox/service_test.go @@ -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) diff --git a/backend/internal/logic/notes/service.go b/backend/internal/logic/notes/service.go index a557396..7899ae1 100644 --- a/backend/internal/logic/notes/service.go +++ b/backend/internal/logic/notes/service.go @@ -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 } diff --git a/backend/internal/logic/projects/cron_handlers.go b/backend/internal/logic/projects/cron_handlers.go index 6d76da0..7216776 100644 --- a/backend/internal/logic/projects/cron_handlers.go +++ b/backend/internal/logic/projects/cron_handlers.go @@ -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, diff --git a/backend/internal/logic/projects/dto.go b/backend/internal/logic/projects/dto.go index 793404b..fa23f00 100644 --- a/backend/internal/logic/projects/dto.go +++ b/backend/internal/logic/projects/dto.go @@ -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, diff --git a/backend/internal/logic/projects/handlers_test.go b/backend/internal/logic/projects/handlers_test.go index f8937fd..e4aea27 100644 --- a/backend/internal/logic/projects/handlers_test.go +++ b/backend/internal/logic/projects/handlers_test.go @@ -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) diff --git a/backend/internal/logic/projects/object_crud.go b/backend/internal/logic/projects/object_crud.go index 492bef9..594cf37 100644 --- a/backend/internal/logic/projects/object_crud.go +++ b/backend/internal/logic/projects/object_crud.go @@ -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 { diff --git a/backend/internal/logic/projects/ownership.go b/backend/internal/logic/projects/ownership.go index f9f00d1..32ac721 100644 --- a/backend/internal/logic/projects/ownership.go +++ b/backend/internal/logic/projects/ownership.go @@ -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 diff --git a/backend/internal/logic/projects/service.go b/backend/internal/logic/projects/service.go index 407def8..60b29ae 100644 --- a/backend/internal/logic/projects/service.go +++ b/backend/internal/logic/projects/service.go @@ -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 } diff --git a/backend/internal/logic/projects/service_test.go b/backend/internal/logic/projects/service_test.go index 2e4edc8..dbd1698 100644 --- a/backend/internal/logic/projects/service_test.go +++ b/backend/internal/logic/projects/service_test.go @@ -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) diff --git a/backend/internal/logic/projects/tag_handlers.go b/backend/internal/logic/projects/tag_handlers.go index 4169ede..1305c8f 100644 --- a/backend/internal/logic/projects/tag_handlers.go +++ b/backend/internal/logic/projects/tag_handlers.go @@ -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", "未登录或登录已失效") diff --git a/backend/internal/logic/projects/workspace.go b/backend/internal/logic/projects/workspace.go index 746b144..4755fce 100644 --- a/backend/internal/logic/projects/workspace.go +++ b/backend/internal/logic/projects/workspace.go @@ -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 diff --git a/backend/internal/logic/projects/workspace_test.go b/backend/internal/logic/projects/workspace_test.go index 6d3bf9e..396a090 100644 --- a/backend/internal/logic/projects/workspace_test.go +++ b/backend/internal/logic/projects/workspace_test.go @@ -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) diff --git a/backend/internal/logic/projects/write_handlers_test.go b/backend/internal/logic/projects/write_handlers_test.go index 6046e68..9ea7f38 100644 --- a/backend/internal/logic/projects/write_handlers_test.go +++ b/backend/internal/logic/projects/write_handlers_test.go @@ -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)) diff --git a/backend/internal/logic/search/handlers_test.go b/backend/internal/logic/search/handlers_test.go index 85f2aba..6746ffe 100644 --- a/backend/internal/logic/search/handlers_test.go +++ b/backend/internal/logic/search/handlers_test.go @@ -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 } diff --git a/backend/internal/logic/search/service.go b/backend/internal/logic/search/service.go index 338ed85..3d0ee9c 100644 --- a/backend/internal/logic/search/service.go +++ b/backend/internal/logic/search/service.go @@ -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 diff --git a/backend/internal/logic/search/service_postgres_test.go b/backend/internal/logic/search/service_postgres_test.go index 5360a3c..984b1e3 100644 --- a/backend/internal/logic/search/service_postgres_test.go +++ b/backend/internal/logic/search/service_postgres_test.go @@ -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, diff --git a/backend/internal/logic/search/service_test.go b/backend/internal/logic/search/service_test.go index 9dd5f56..b2e93f6 100644 --- a/backend/internal/logic/search/service_test.go +++ b/backend/internal/logic/search/service_test.go @@ -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) diff --git a/backend/internal/logic/tasks/concurrency_postgres_test.go b/backend/internal/logic/tasks/concurrency_postgres_test.go index ee688a0..950742f 100644 --- a/backend/internal/logic/tasks/concurrency_postgres_test.go +++ b/backend/internal/logic/tasks/concurrency_postgres_test.go @@ -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) } diff --git a/backend/internal/logic/tasks/handlers_test.go b/backend/internal/logic/tasks/handlers_test.go index ebb7e69..e132d61 100644 --- a/backend/internal/logic/tasks/handlers_test.go +++ b/backend/internal/logic/tasks/handlers_test.go @@ -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"}, diff --git a/backend/internal/logic/tasks/service.go b/backend/internal/logic/tasks/service.go index 3c78054..a935104 100644 --- a/backend/internal/logic/tasks/service.go +++ b/backend/internal/logic/tasks/service.go @@ -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 { diff --git a/backend/internal/logic/tasks/service_test.go b/backend/internal/logic/tasks/service_test.go index 52aa619..d46113e 100644 --- a/backend/internal/logic/tasks/service_test.go +++ b/backend/internal/logic/tasks/service_test.go @@ -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" diff --git a/backend/internal/models/ai_call_log.go b/backend/internal/models/ai_call_log.go index dfddb20..e23bae5 100644 --- a/backend/internal/models/ai_call_log.go +++ b/backend/internal/models/ai_call_log.go @@ -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" } diff --git a/backend/internal/models/ai_expert_category.go b/backend/internal/models/ai_expert_category.go index 718a0c2..6efa8e5 100644 --- a/backend/internal/models/ai_expert_category.go +++ b/backend/internal/models/ai_expert_category.go @@ -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" } diff --git a/backend/internal/models/ai_expert_item.go b/backend/internal/models/ai_expert_item.go index 6c91ecf..3f8ac99 100644 --- a/backend/internal/models/ai_expert_item.go +++ b/backend/internal/models/ai_expert_item.go @@ -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" } diff --git a/backend/internal/models/ai_key.go b/backend/internal/models/ai_key.go index a19ed05..b8ec02f 100644 --- a/backend/internal/models/ai_key.go +++ b/backend/internal/models/ai_key.go @@ -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" } diff --git a/backend/internal/models/ai_rate_bucket.go b/backend/internal/models/ai_rate_bucket.go index d2fd234..f81993e 100644 --- a/backend/internal/models/ai_rate_bucket.go +++ b/backend/internal/models/ai_rate_bucket.go @@ -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" } diff --git a/backend/internal/models/ai_session.go b/backend/internal/models/ai_session.go index 182495c..d877aea 100644 --- a/backend/internal/models/ai_session.go +++ b/backend/internal/models/ai_session.go @@ -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" } diff --git a/backend/internal/models/cron_plan.go b/backend/internal/models/cron_plan.go index 9569a4b..e681ce6 100644 --- a/backend/internal/models/cron_plan.go +++ b/backend/internal/models/cron_plan.go @@ -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" } diff --git a/backend/internal/models/identity.go b/backend/internal/models/identity.go index 33e66fa..b57603a 100644 --- a/backend/internal/models/identity.go +++ b/backend/internal/models/identity.go @@ -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 } diff --git a/backend/internal/models/identity_test.go b/backend/internal/models/identity_test.go index 5c4d8cc..72dfe60 100644 --- a/backend/internal/models/identity_test.go +++ b/backend/internal/models/identity_test.go @@ -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) diff --git a/backend/internal/models/inbox_item.go b/backend/internal/models/inbox_item.go index d88fd31..7f9b2b8 100644 --- a/backend/internal/models/inbox_item.go +++ b/backend/internal/models/inbox_item.go @@ -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" } diff --git a/backend/internal/models/inbox_suggestion.go b/backend/internal/models/inbox_suggestion.go index a06fc05..f287561 100644 --- a/backend/internal/models/inbox_suggestion.go +++ b/backend/internal/models/inbox_suggestion.go @@ -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" } diff --git a/backend/internal/models/legacy_tables.go b/backend/internal/models/legacy_tables.go new file mode 100644 index 0000000..0fe6844 --- /dev/null +++ b/backend/internal/models/legacy_tables.go @@ -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 +} diff --git a/backend/internal/models/migrations.go b/backend/internal/models/migrations.go index d6b738d..52a4033 100644 --- a/backend/internal/models/migrations.go +++ b/backend/internal/models/migrations.go @@ -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 diff --git a/backend/internal/models/migrations_test.go b/backend/internal/models/migrations_test.go index 9e59a46..7ca323f 100644 --- a/backend/internal/models/migrations_test.go +++ b/backend/internal/models/migrations_test.go @@ -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) diff --git a/backend/internal/models/new.go b/backend/internal/models/new.go index 09cd170..11edd22 100644 --- a/backend/internal/models/new.go +++ b/backend/internal/models/new.go @@ -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) } diff --git a/backend/internal/models/note.go b/backend/internal/models/note.go index 885f18f..9bfe65a 100644 --- a/backend/internal/models/note.go +++ b/backend/internal/models/note.go @@ -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" } diff --git a/backend/internal/models/project.go b/backend/internal/models/project.go index 80cffea..d95bf46 100644 --- a/backend/internal/models/project.go +++ b/backend/internal/models/project.go @@ -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" } diff --git a/backend/internal/models/project_channel.go b/backend/internal/models/project_channel.go index 9ef4b5b..2bf5337 100644 --- a/backend/internal/models/project_channel.go +++ b/backend/internal/models/project_channel.go @@ -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" } diff --git a/backend/internal/models/project_event.go b/backend/internal/models/project_event.go index 5984af0..8468e3c 100644 --- a/backend/internal/models/project_event.go +++ b/backend/internal/models/project_event.go @@ -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" } diff --git a/backend/internal/models/source.go b/backend/internal/models/source.go index 6eab6e0..8d9eeae 100644 --- a/backend/internal/models/source.go +++ b/backend/internal/models/source.go @@ -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" } diff --git a/backend/internal/models/tag.go b/backend/internal/models/tag.go index f143b4d..18762a2 100644 --- a/backend/internal/models/tag.go +++ b/backend/internal/models/tag.go @@ -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" } diff --git a/backend/internal/models/task.go b/backend/internal/models/task.go index 02a7d78..685a969 100644 --- a/backend/internal/models/task.go +++ b/backend/internal/models/task.go @@ -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" } diff --git a/backend/internal/models/task_share.go b/backend/internal/models/task_share.go index c02094a..c97db6e 100644 --- a/backend/internal/models/task_share.go +++ b/backend/internal/models/task_share.go @@ -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" } diff --git a/backend/internal/models/user.go b/backend/internal/models/user.go index 86a07d9..f14c955 100644 --- a/backend/internal/models/user.go +++ b/backend/internal/models/user.go @@ -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" } diff --git a/backend/internal/seed/demo.go b/backend/internal/seed/demo.go index ebd316b..c9ec9f0 100644 --- a/backend/internal/seed/demo.go +++ b/backend/internal/seed/demo.go @@ -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 { diff --git a/backend/internal/seed/demo_postgres_test.go b/backend/internal/seed/demo_postgres_test.go index bf9aa7a..ccb7388 100644 --- a/backend/internal/seed/demo_postgres_test.go +++ b/backend/internal/seed/demo_postgres_test.go @@ -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) } diff --git a/backend/internal/seed/demo_test.go b/backend/internal/seed/demo_test.go index 6d0e10b..61e985f 100644 --- a/backend/internal/seed/demo_test.go +++ b/backend/internal/seed/demo_test.go @@ -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) } diff --git a/backend/migrations/0001_search_indexes.sql b/backend/migrations/0001_search_indexes.sql index c299f10..01b70d7 100644 --- a/backend/migrations/0001_search_indexes.sql +++ b/backend/migrations/0001_search_indexes.sql @@ -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, ''))); diff --git a/docs/superpowers/plans/2026-07-21-forest-ai-api-backend.md b/docs/superpowers/plans/2026-07-21-forest-ai-api-backend.md index 7a019d0..a01df9e 100644 --- a/docs/superpowers/plans/2026-07-21-forest-ai-api-backend.md +++ b/docs/superpowers/plans/2026-07-21-forest-ai-api-backend.md @@ -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} } ``` diff --git a/docs/superpowers/specs/2026-07-21-senlinai-mvp-unification-design.md b/docs/superpowers/specs/2026-07-21-senlinai-mvp-unification-design.md index 074cf20..9bbf0e1 100644 --- a/docs/superpowers/specs/2026-07-21-senlinai-mvp-unification-design.md +++ b/docs/superpowers/specs/2026-07-21-senlinai-mvp-unification-design.md @@ -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 用户、密码、数据库名和端口。