feat: align controlled AI sessions and MVP controls
This commit is contained in:
@@ -2,23 +2,118 @@ package ai
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"senlinai-agent/backend/internal/logic/projects"
|
||||
"senlinai-agent/backend/internal/models"
|
||||
)
|
||||
|
||||
const (
|
||||
aiSessionCreateAction = "ai_session_create"
|
||||
aiSessionCreateLimit = 20
|
||||
defaultSessionStatus = "ready"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidSession = errors.New("invalid ai session")
|
||||
)
|
||||
|
||||
type sessionGateway interface {
|
||||
CheckRateLimit(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
|
||||
}
|
||||
|
||||
// SessionService 只管理项目内普通 AI 会话;它不会把上下文自动转换为任务、笔记或资料。
|
||||
type SessionService struct {
|
||||
gateway sessionGateway
|
||||
}
|
||||
|
||||
func NewSessionService() *SessionService {
|
||||
return &SessionService{}
|
||||
func NewSessionService(gateway sessionGateway) *SessionService {
|
||||
return &SessionService{gateway: gateway}
|
||||
}
|
||||
|
||||
func (s *SessionService) Create(projectID uint, userID uint, title string) (*models.SenlinAgentAISession, error) {
|
||||
title = strings.TrimSpace(title)
|
||||
if title == "" {
|
||||
return nil, errors.New("session title is required")
|
||||
// List 在项目 owner 校验后返回该项目的会话,内部自增 ID 不离开服务边界。
|
||||
func (s *SessionService) List(userID uint, projectIdentity string) ([]models.SenlinAgentAISession, error) {
|
||||
project, err := projects.FindOwnedProject(userID, projectIdentity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
session := &models.SenlinAgentAISession{ProjectID: projectID, CreatedBy: userID, Title: title}
|
||||
return session, models.DBService.Create(session).Error
|
||||
var sessions []models.SenlinAgentAISession
|
||||
if err := models.DBService.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{}
|
||||
}
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
// Create 先校验项目,再限流,最后才选择 provider/key;会话创建不会生成任何正式业务对象。
|
||||
func (s *SessionService) Create(userID uint, projectIdentity, title, context string) (*models.SenlinAgentAISession, error) {
|
||||
title = strings.TrimSpace(title)
|
||||
context = strings.TrimSpace(context)
|
||||
if title == "" {
|
||||
return nil, ErrInvalidSession
|
||||
}
|
||||
project, err := projects.FindOwnedProject(userID, projectIdentity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.gateway == nil {
|
||||
return nil, errors.New("ai gateway is required")
|
||||
}
|
||||
|
||||
if err := s.gateway.CheckRateLimit(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 {
|
||||
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 {
|
||||
return nil, fmt.Errorf("check rate limit: %v; record failure: %w", err, auditErr)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
selected, err := s.gateway.SelectKey(userID)
|
||||
if err != nil {
|
||||
code := "provider_selection_failed"
|
||||
if errors.Is(err, ErrAIKeyMissing) {
|
||||
code = "ai_key_missing"
|
||||
}
|
||||
if auditErr := s.gateway.RecordCall(userID, "none", "none", aiSessionCreateAction, "failed", code); auditErr != nil {
|
||||
return nil, fmt.Errorf("select ai key: %v; record failure: %w", err, auditErr)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session := models.SenlinAgentAISession{
|
||||
ProjectID: project.ID,
|
||||
CreatedBy: userID,
|
||||
Title: title,
|
||||
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)
|
||||
}
|
||||
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 aiSessionStatus(session models.SenlinAgentAISession) string {
|
||||
if status := strings.TrimSpace(session.Status); status != "" {
|
||||
return status
|
||||
}
|
||||
return defaultSessionStatus
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user