fix: harden controlled AI session consistency

This commit is contained in:
2026-07-21 19:54:25 +08:00
parent 122f5d8c52
commit 251b212e06
15 changed files with 406 additions and 62 deletions

View File

@@ -6,6 +6,7 @@ import (
"strings"
"time"
"gorm.io/gorm"
"senlinai-agent/backend/internal/logic/projects"
"senlinai-agent/backend/internal/models"
)
@@ -21,9 +22,9 @@ var (
)
type sessionGateway interface {
CheckRateLimit(userID uint, action string, limit int, window time.Duration) error
ReserveRateLimit(userID uint, action string, limit int, window time.Duration) error
SelectKey(userID uint) (SelectedKey, error)
RecordCall(userID uint, provider string, usedKeyType string, action string, status string, errText string) error
RecordCall(database *gorm.DB, userID uint, provider string, usedKeyType string, action string, status string, errText string) error
}
// SessionService 只管理项目内普通 AI 会话;它不会把上下文自动转换为任务、笔记或资料。
@@ -66,14 +67,14 @@ func (s *SessionService) Create(userID uint, projectIdentity, title, context str
return nil, errors.New("ai gateway is required")
}
if err := s.gateway.CheckRateLimit(userID, aiSessionCreateAction, aiSessionCreateLimit, time.Hour); err != nil {
if err := s.gateway.ReserveRateLimit(userID, aiSessionCreateAction, aiSessionCreateLimit, time.Hour); err != nil {
if errors.Is(err, ErrAIRateLimited) {
if auditErr := s.gateway.RecordCall(userID, "none", "none", aiSessionCreateAction, "failed", "ai_rate_limited"); auditErr != nil {
if auditErr := s.gateway.RecordCall(models.DBService, userID, "none", "none", aiSessionCreateAction, "failed", "ai_rate_limited"); auditErr != nil {
return nil, fmt.Errorf("record ai rate limit failure: %w", auditErr)
}
return nil, ErrAIRateLimited
}
if auditErr := s.gateway.RecordCall(userID, "none", "none", aiSessionCreateAction, "failed", "rate_limit_check_failed"); auditErr != nil {
if auditErr := s.gateway.RecordCall(models.DBService, userID, "none", "none", aiSessionCreateAction, "failed", "rate_limit_reservation_failed"); auditErr != nil {
return nil, fmt.Errorf("check rate limit: %v; record failure: %w", err, auditErr)
}
return nil, err
@@ -85,7 +86,8 @@ func (s *SessionService) Create(userID uint, projectIdentity, title, context str
if errors.Is(err, ErrAIKeyMissing) {
code = "ai_key_missing"
}
if auditErr := s.gateway.RecordCall(userID, "none", "none", aiSessionCreateAction, "failed", code); auditErr != nil {
provider, keyType := selectedAuditMetadata(selected)
if auditErr := s.gateway.RecordCall(models.DBService, userID, provider, keyType, aiSessionCreateAction, "failed", code); auditErr != nil {
return nil, fmt.Errorf("select ai key: %v; record failure: %w", err, auditErr)
}
return nil, err
@@ -98,19 +100,40 @@ func (s *SessionService) Create(userID uint, projectIdentity, title, context str
Context: context,
Status: defaultSessionStatus,
}
if err := models.DBService.Create(&session).Error; err != nil {
if auditErr := s.gateway.RecordCall(userID, selected.Provider, selected.KeyType, aiSessionCreateAction, "failed", "session_create_failed"); auditErr != nil {
return nil, fmt.Errorf("create ai session: %v; record failure: %w", err, auditErr)
failureCode := "session_create_failed"
err = models.DBService.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&session).Error; err != nil {
return err
}
// ready 只表示会话入口已建立,不表示 provider 已回复或任何业务对象已创建。
failureCode = "audit_write_failed"
if err := s.gateway.RecordCall(tx, userID, selected.Provider, selected.KeyType, aiSessionCreateAction, defaultSessionStatus, ""); err != nil {
return err
}
failureCode = "session_transaction_failed"
return nil
})
if err != nil {
if auditErr := s.gateway.RecordCall(models.DBService, userID, selected.Provider, selected.KeyType, aiSessionCreateAction, "failed", failureCode); auditErr != nil {
return nil, fmt.Errorf("create ai session transaction: %v; record failure: %w", err, auditErr)
}
return nil, err
}
// ready 只表示会话入口已建立,不表示 provider 已回复或任何业务对象已创建。
if err := s.gateway.RecordCall(userID, selected.Provider, selected.KeyType, aiSessionCreateAction, defaultSessionStatus, ""); err != nil {
return nil, fmt.Errorf("record ai session creation: %w", err)
}
return &session, nil
}
func selectedAuditMetadata(selected SelectedKey) (string, string) {
provider := strings.TrimSpace(selected.Provider)
keyType := strings.TrimSpace(selected.KeyType)
if provider == "" {
provider = "none"
}
if keyType == "" {
keyType = "none"
}
return provider, keyType
}
func aiSessionStatus(session models.SenlinAgentAISession) string {
if status := strings.TrimSpace(session.Status); status != "" {
return status