package ai import ( "errors" "log" "net/http" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" "senlinai-agent/backend/internal/httpx" "senlinai-agent/backend/internal/logic/auth" "senlinai-agent/backend/internal/models" ) // SessionDTO 是普通项目 AI 会话的公开契约,不携带数据库主键或自动创建对象的 ID。 type SessionDTO struct { ID string `json:"id"` ProjectID string `json:"projectId"` Title string `json:"title"` Context string `json:"context"` Status string `json:"status"` Expert *ExpertSummaryDTO `json:"expert"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type createSessionRequest struct { Title string `json:"title"` Context string `json:"context"` ExpertID string `json:"expertId"` } type ExpertSummaryDTO struct { ID string `json:"id"` Slug string `json:"slug"` Category string `json:"category"` CategoryName string `json:"categoryName"` Name string `json:"name"` Description string `json:"description"` Emoji string `json:"emoji"` Color string `json:"color"` } type ExpertDetailDTO struct { ExpertSummaryDTO SystemPrompt string `json:"systemPrompt"` Source string `json:"source"` SourceLicense string `json:"sourceLicense"` } // Handler 注册受认证、受项目所有权保护的 AI 会话接口。 type Handler struct { service *SessionService } func NewHandler(service *SessionService) *Handler { return &Handler{service: service} } func (h *Handler) Register(router gin.IRouter) { router.GET("/ai-experts", h.listExperts) router.GET("/ai-experts/:id", h.getExpert) router.GET("/projects/:id/ai-sessions", h.list) router.POST("/projects/:id/ai-sessions", h.create) } func (h *Handler) list(c *gin.Context) { userID, projectIdentity, ok := aiRequestContext(c) if !ok { return } sessions, err := h.service.List(userID, projectIdentity) if err != nil { writeAIError(c, err) return } items := make([]SessionDTO, 0, len(sessions)) for _, session := range sessions { items = append(items, sessionDTO(session)) } c.JSON(http.StatusOK, items) } func (h *Handler) create(c *gin.Context) { userID, projectIdentity, ok := aiRequestContext(c) if !ok { return } var input createSessionRequest if err := c.ShouldBindJSON(&input); err != nil { httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效") return } session, err := h.service.CreateWithExpert(userID, projectIdentity, input.Title, input.Context, input.ExpertID) if err != nil { writeAIError(c, err) return } c.JSON(http.StatusCreated, sessionDTO(*session)) } func (h *Handler) listExperts(c *gin.Context) { experts, err := h.service.ListExperts(ExpertFilter{Category: c.Query("category"), Query: c.Query("q")}) if err != nil { writeAIError(c, err) return } items := make([]ExpertSummaryDTO, 0, len(experts)) for _, expert := range experts { items = append(items, expertSummaryDTO(expert)) } c.JSON(http.StatusOK, items) } func (h *Handler) getExpert(c *gin.Context) { identity, ok := httpx.IdentityParam(c, "id") if !ok { return } expert, err := h.service.GetExpert(identity) if err != nil { writeAIError(c, err) return } c.JSON(http.StatusOK, ExpertDetailDTO{ ExpertSummaryDTO: expertSummaryDTO(*expert), SystemPrompt: expert.SystemPrompt, Source: expert.Source, SourceLicense: expert.SourceLicense, }) } func aiRequestContext(c *gin.Context) (uint, string, bool) { userID, ok := auth.CurrentUserID(c) if !ok { httpx.Error(c, http.StatusUnauthorized, "unauthorized", "未登录或登录已失效") return 0, "", false } projectIdentity, ok := httpx.IdentityParam(c, "id") if !ok { return 0, "", false } return userID, projectIdentity, true } func sessionDTO(session models.SaAISession) SessionDTO { dto := SessionDTO{ ID: session.Identity, ProjectID: session.ProjectIdentity, Title: session.Title, Context: session.Context, Status: aiSessionStatus(session), CreatedAt: session.CreatedAt.UTC(), UpdatedAt: session.UpdatedAt.UTC(), } if session.Expert != nil { dto.Expert = expertSummaryPointer(*session.Expert) } return dto } func expertSummaryPointer(expert models.SaAIExpertItem) *ExpertSummaryDTO { dto := expertSummaryDTO(expert) return &dto } 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, Emoji: expert.Emoji, Color: expert.Color, } } func writeAIError(c *gin.Context, err error) { switch { case errors.Is(err, ErrExpertNotFound): httpx.Error(c, http.StatusNotFound, "expert_not_found", "专家不存在或已停用") case errors.Is(err, gorm.ErrRecordNotFound): httpx.Error(c, http.StatusNotFound, "not_found", "项目不存在或无权访问") case errors.Is(err, ErrInvalidSession): httpx.Error(c, http.StatusBadRequest, "invalid_request", "请输入 AI 会话标题") case errors.Is(err, ErrAIRateLimited): httpx.Error(c, http.StatusTooManyRequests, "ai_rate_limited", "AI 请求过于频繁,请稍后重试") case errors.Is(err, ErrAIKeyMissing): httpx.Error(c, http.StatusServiceUnavailable, "ai_key_missing", "尚未配置可用的 AI 密钥") default: log.Printf("ai session request failed: %v", err) httpx.Error(c, http.StatusInternalServerError, "internal_error", "AI 会话操作失败,请稍后重试") } }