feat: align controlled AI sessions and MVP controls

This commit is contained in:
2026-07-21 19:19:36 +08:00
parent 8767446b78
commit 122f5d8c52
18 changed files with 758 additions and 468 deletions

View File

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