refactor: shorten model and table prefixes

This commit is contained in:
2026-07-23 14:30:17 +08:00
parent 6e7be2f3a4
commit 0ca2898ac0
71 changed files with 707 additions and 638 deletions

View File

@@ -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 阶段不要引入全局标签体系。

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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),

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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"))

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"})

View File

@@ -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")
}

View File

@@ -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)),

View File

@@ -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)

View File

@@ -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,
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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(&note).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)

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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", "未登录或登录已失效")

View File

@@ -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(&noteCount).Error; err != nil {
if err := models.DBService.Model(&models.SaNote{}).Where("project_id = ?", projectID).Count(&noteCount).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(&notes).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

View File

@@ -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, &note, &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)

View File

@@ -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))

View File

@@ -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(&note).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
}

View File

@@ -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(&notes).Error; err != nil {
return nil, err

View File

@@ -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,

View File

@@ -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)

View File

@@ -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(&note).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(&note)
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)
}

View File

@@ -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(&note).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"},

View File

@@ -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 {

View File

@@ -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(&note).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(&note).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(&note).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"

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -0,0 +1,48 @@
package models
import (
"fmt"
"gorm.io/gorm"
)
var legacyTableRenames = []struct {
legacy string
current string
}{
{legacy: "senlin_agent_schema_migrations", current: "sa_schema_migrations"},
{legacy: "senlin_agent_users", current: "sa_users"},
{legacy: "senlin_agent_projects", current: "sa_projects"},
{legacy: "senlin_agent_inbox_items", current: "sa_inbox_items"},
{legacy: "senlin_agent_inbox_suggestions", current: "sa_inbox_suggestions"},
{legacy: "senlin_agent_tasks", current: "sa_tasks"},
{legacy: "senlin_agent_notes", current: "sa_notes"},
{legacy: "senlin_agent_sources", current: "sa_sources"},
{legacy: "senlin_agent_ai_expert_categories", current: "sa_ai_expert_categories"},
{legacy: "senlin_agent_ai_experts", current: "sa_ai_experts"},
{legacy: "senlin_agent_ai_sessions", current: "sa_ai_sessions"},
{legacy: "senlin_agent_tags", current: "sa_tags"},
{legacy: "senlin_agent_project_channels", current: "sa_project_channels"},
{legacy: "senlin_agent_cron_plans", current: "sa_cron_plans"},
{legacy: "senlin_agent_project_events", current: "sa_project_events"},
{legacy: "senlin_agent_ai_keys", current: "sa_ai_keys"},
{legacy: "senlin_agent_ai_call_logs", current: "sa_ai_call_logs"},
{legacy: "senlin_agent_ai_rate_buckets", current: "sa_ai_rate_buckets"},
{legacy: "senlin_agent_task_shares", current: "sa_task_shares"},
}
func renameLegacyTables(database *gorm.DB) error {
migrator := database.Migrator()
for _, table := range legacyTableRenames {
if !migrator.HasTable(table.legacy) {
continue
}
if migrator.HasTable(table.current) {
return fmt.Errorf("cannot rename legacy table %s: target table %s already exists", table.legacy, table.current)
}
if err := migrator.RenameTable(table.legacy, table.current); err != nil {
return fmt.Errorf("rename legacy table %s to %s: %w", table.legacy, table.current, err)
}
}
return nil
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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, '')));

View File

@@ -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}
}
```

View File

@@ -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 用户、密码、数据库名和端口。